Add CTS tests for WaveformBuilder

Add tests for VibrationEffect created with the WaveformBuilder APIs and
also introduce Vibrator/VibratorManager tests for playing effects with
frequency changes.

Bug: 211750173
Test: VibrationEffectTest
Change-Id: I6b82bb27aba68bcc080389061190d9a1228b7bdb
diff --git a/tests/tests/os/src/android/os/cts/VibrationEffectTest.java b/tests/tests/os/src/android/os/cts/VibrationEffectTest.java
index e34ae5d..a3799ce 100644
--- a/tests/tests/os/src/android/os/cts/VibrationEffectTest.java
+++ b/tests/tests/os/src/android/os/cts/VibrationEffectTest.java
@@ -16,6 +16,9 @@
 
 package android.os.cts;
 
+import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
+import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
@@ -25,6 +28,7 @@
 
 import android.os.Parcel;
 import android.os.VibrationEffect;
+import android.os.VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.RampSegment;
@@ -37,6 +41,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.time.Duration;
 import java.util.Arrays;
 
 @SmallTest
@@ -49,9 +54,9 @@
 
     private static final long[] TEST_TIMINGS = new long[]{100, 100, 200};
     private static final int[] TEST_AMPLITUDES =
-            new int[]{255, 0, VibrationEffect.DEFAULT_AMPLITUDE};
+            new int[]{255, 0, 102};
     private static final float[] TEST_FLOAT_AMPLITUDES =
-            new float[]{1f, 0f, VibrationEffect.DEFAULT_AMPLITUDE};
+            new float[]{1f, 0f, 0.4f};
 
     private static final VibrationEffect TEST_ONE_SHOT =
             VibrationEffect.createOneShot(TEST_TIMING, TEST_AMPLITUDE);
@@ -60,11 +65,13 @@
     private static final VibrationEffect TEST_WAVEFORM_NO_AMPLITUDES =
             VibrationEffect.createWaveform(TEST_TIMINGS, -1);
     private static final VibrationEffect TEST_WAVEFORM_BUILT =
-            VibrationEffect.startWaveform()
-                    .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
-                    .addStep(/* amplitude= */ 0.8f, /* frequency= */ 100f, /* duration= */ 20)
-                    .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
-                    .addRamp(/* amplitude= */ 0.2f, /* frequency= */ 200f, /* duration= */ 200)
+            VibrationEffect.startWaveform(targetAmplitude(0.5f))
+                    .addSustain(Duration.ofMillis(10))
+                    .addTransition(Duration.ZERO, targetAmplitude(0.8f), targetFrequency(100f))
+                    .addSustain(Duration.ofMillis(10))
+                    .addTransition(Duration.ofMillis(100), targetAmplitude(1))
+                    .addTransition(Duration.ofMillis(200),
+                            targetAmplitude(0.2f), targetFrequency(200f))
                     .build();
     private static final VibrationEffect TEST_PREBAKED =
             VibrationEffect.get(VibrationEffect.EFFECT_CLICK, true);
@@ -74,8 +81,10 @@
                     .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.8f)
                     .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, /* delay= */ 10)
                     .addEffect(TEST_ONE_SHOT)
-                    .addEffect(TEST_WAVEFORM, /* delay= */ 10)
-                    .addEffect(TEST_WAVEFORM_BUILT, /* delay= */ 100)
+                    .addOffDuration(Duration.ofMillis(10))
+                    .addEffect(TEST_WAVEFORM)
+                    .addOffDuration(Duration.ofSeconds(1))
+                    .addEffect(TEST_WAVEFORM_BUILT)
                     .compose();
 
 
@@ -186,6 +195,10 @@
             assertAmplitude(TEST_FLOAT_AMPLITUDES[i], effect, i);
         }
 
+        effect = VibrationEffect.createWaveform(new long[] { 10 },
+                new int[] { VibrationEffect.DEFAULT_AMPLITUDE }, -1);
+        assertAmplitude(VibrationEffect.DEFAULT_AMPLITUDE, effect, /* index= */ 0);
+
         effect = VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, 0);
         assertEquals(0, getRepeatIndex(effect));
 
@@ -417,14 +430,16 @@
     public void testComposedEquals() {
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                .addEffect(TEST_ONE_SHOT, /* delay= */ 10)
+                .addOffDuration(Duration.ofMillis(10))
+                .addEffect(TEST_ONE_SHOT)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 10)
                 .addEffect(TEST_WAVEFORM)
                 .compose();
 
         VibrationEffect otherEffect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 0)
-                .addEffect(TEST_ONE_SHOT, /* delay= */ 10)
+                .addOffDuration(Duration.ofMillis(10))
+                .addEffect(TEST_ONE_SHOT)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 10)
                 .addEffect(TEST_WAVEFORM)
                 .compose();
@@ -433,6 +448,23 @@
     }
 
     @Test
+    public void testComposedRepeatingAreEqualsAreEquals() {
+        VibrationEffect repeatingWaveform = VibrationEffect.createWaveform(
+                new long[] { 10, 20, 30}, new int[] { 50, 100, 150 }, /* repeatIndex= */ 0);
+        VibrationEffect nonRepeatingWaveform = VibrationEffect.createWaveform(
+                new long[] { 10, 20, 30}, new int[] { 50, 100, 150 }, /* repeatIndex= */ -1);
+
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addEffect(repeatingWaveform)
+                .compose();
+        VibrationEffect otherEffect = VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(nonRepeatingWaveform)
+                .compose();
+        assertEquals(effect, otherEffect);
+        assertEquals(effect.hashCode(), otherEffect.hashCode());
+    }
+
+    @Test
     public void testComposedDifferentPrimitivesNotEquals() {
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
@@ -504,10 +536,12 @@
     @Test
     public void testComposedDifferentWaveformDelayNotEquals() {
         VibrationEffect effect = VibrationEffect.startComposition()
-                .addEffect(TEST_ONE_SHOT, /* delay= */ 10)
+                .addOffDuration(Duration.ofMillis(10))
+                .addEffect(TEST_ONE_SHOT)
                 .compose();
         VibrationEffect otherEffect = VibrationEffect.startComposition()
-                .addEffect(TEST_ONE_SHOT, /* delay= */ 100)
+                .addOffDuration(Duration.ofSeconds(10))
+                .addEffect(TEST_ONE_SHOT)
                 .compose();
         assertNotEquals(effect, otherEffect);
     }
@@ -526,11 +560,19 @@
                 .compose();
         assertEquals(TEST_ONE_SHOT.getDuration(), effect.getDuration());
 
+        effect = VibrationEffect.startComposition().addOffDuration(Duration.ofSeconds(2)).compose();
+        assertEquals(2_000, effect.getDuration());
+
         effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
                 .addEffect(VibrationEffect.createWaveform(new long[]{10, 10}, /* repeat= */ 0))
                 .compose();
         assertEquals(Long.MAX_VALUE, effect.getDuration());
+
+        effect = VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(TEST_ONE_SHOT)
+                .compose();
+        assertEquals(Long.MAX_VALUE, effect.getDuration());
     }
 
     @Test(expected = IllegalStateException.class)
@@ -538,17 +580,55 @@
         VibrationEffect.startComposition().compose();
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testComposeRepeatEffectWithRepeatingEffectIsInvalid() {
+        VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(
+                        VibrationEffect.createWaveform(new long[] { 10 }, new int[] { 255 }, 0))
+                .compose();
+    }
+
+    @Test(expected = UnreachableAfterRepeatingIndefinitelyException.class)
+    public void testComposeAddOffDurationAfterRepeatingEffectIsInvalid() {
+        VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(TEST_ONE_SHOT)
+                .addOffDuration(Duration.ofMillis(20));
+    }
+
+    @Test(expected = UnreachableAfterRepeatingIndefinitelyException.class)
+    public void testComposeAddRepeatingEffectAfterRepeatingEffectIsInvalid() {
+        VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(TEST_ONE_SHOT)
+                .repeatEffectIndefinitely(TEST_ONE_SHOT);
+    }
+
+    @Test(expected = UnreachableAfterRepeatingIndefinitelyException.class)
+    public void testComposeAddEffectAfterRepeatingEffectIsInvalid() {
+        VibrationEffect.startComposition()
+                .addEffect(
+                        VibrationEffect.createWaveform(new long[] { 10 }, new int[] { 255 }, 0))
+                .addEffect(TEST_PREBAKED);
+    }
+
+    @Test(expected = UnreachableAfterRepeatingIndefinitelyException.class)
+    public void testComposeAddPrimitiveAfterRepeatingEffectIsInvalid() {
+        VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(TEST_ONE_SHOT)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK);
+    }
+
     @Test
     public void testStartWaveform() {
         VibrationEffect.WaveformBuilder first = VibrationEffect.startWaveform();
         VibrationEffect.WaveformBuilder other = VibrationEffect.startWaveform();
         assertNotEquals(first, other);
 
-        VibrationEffect effect = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
-                .addStep(/* amplitude= */ 0.8f, /* frequency= */ 100f, /* duration= */ 20)
-                .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
-                .addRamp(/* amplitude= */ 0.2f, /* frequency= */ 200f, /* duration= */ 200)
+        VibrationEffect effect = VibrationEffect.startWaveform(targetAmplitude(0.5f))
+                .addSustain(Duration.ofMillis(10))
+                .addTransition(Duration.ZERO, targetAmplitude(0.8f), targetFrequency(100f))
+                .addSustain(Duration.ofMillis(20))
+                .addTransition(Duration.ofMillis(100), targetAmplitude(1))
+                .addTransition(Duration.ofMillis(200), targetAmplitude(0.2f), targetFrequency(200f))
                 .build();
 
         assertArrayEquals(new long[]{10, 20, 100, 200}, getTimings(effect));
@@ -572,40 +652,58 @@
     @Test
     public void testStartWaveformEquals() {
         VibrationEffect other = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
-                .addStep(/* amplitude= */ 0.8f, /* frequency= */ 100f, /* duration= */ 20)
-                .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
-                .addRamp(/* amplitude= */ 0.2f, /* frequency= */ 200f, /* duration= */ 200)
+                .addTransition(Duration.ZERO, targetAmplitude(0.5f))
+                .addSustain(Duration.ofMillis(10))
+                .addTransition(Duration.ZERO, targetAmplitude(0.8f), targetFrequency(100f))
+                .addSustain(Duration.ofMillis(10))
+                .addTransition(Duration.ofMillis(100), targetAmplitude(1))
+                .addTransition(Duration.ofMillis(200),
+                        targetAmplitude(0.2f), targetFrequency(200f))
                 .build();
+
         assertEquals(TEST_WAVEFORM_BUILT, other);
         assertEquals(TEST_WAVEFORM_BUILT.hashCode(), other.hashCode());
 
-        VibrationEffect.WaveformBuilder builder = VibrationEffect.startWaveform()
-                .addStep(TEST_FLOAT_AMPLITUDE, (int) TEST_TIMING);
+        VibrationEffect.WaveformBuilder builder =
+                VibrationEffect.startWaveform(targetAmplitude(TEST_FLOAT_AMPLITUDE))
+                .addSustain(Duration.ofMillis(TEST_TIMING));
         assertEquals(TEST_ONE_SHOT, builder.build());
         assertEquals(TEST_ONE_SHOT.hashCode(), builder.build().hashCode());
 
         builder = VibrationEffect.startWaveform();
         for (int i = 0; i < TEST_TIMINGS.length; i++) {
-            builder.addStep(i % 2 == 0 ? 0 : VibrationEffect.DEFAULT_AMPLITUDE,
-                    (int) TEST_TIMINGS[i]);
-        }
-        assertEquals(TEST_WAVEFORM_NO_AMPLITUDES, builder.build());
-        assertEquals(TEST_WAVEFORM_NO_AMPLITUDES.hashCode(), builder.build().hashCode());
-
-        builder = VibrationEffect.startWaveform();
-        for (int i = 0; i < TEST_TIMINGS.length; i++) {
-            builder.addStep(TEST_FLOAT_AMPLITUDES[i], (int) TEST_TIMINGS[i]);
+            builder.addTransition(Duration.ZERO, targetAmplitude(TEST_FLOAT_AMPLITUDES[i]));
+            builder.addSustain(Duration.ofMillis(TEST_TIMINGS[i]));
         }
         assertEquals(TEST_WAVEFORM, builder.build());
         assertEquals(TEST_WAVEFORM.hashCode(), builder.build().hashCode());
     }
 
     @Test
+    public void testStartWaveformEqualsSustainCreatedViaTransitions() {
+        VibrationEffect effect = VibrationEffect.startWaveform()
+                .addTransition(Duration.ZERO, targetAmplitude(0.5f))
+                .addSustain(Duration.ofMillis(10))
+                .build();
+        VibrationEffect other = VibrationEffect.startWaveform(targetAmplitude(0.5f))
+                .addSustain(Duration.ofMillis(10))
+                .build();
+        assertEquals(effect, other);
+
+        effect = VibrationEffect.startWaveform(targetAmplitude(1f), targetFrequency(100f))
+                .addTransition(Duration.ofMillis(10), targetAmplitude(1f), targetFrequency(100f))
+                .build();
+        other = VibrationEffect.startWaveform(targetAmplitude(1f), targetFrequency(100f))
+                .addSustain(Duration.ofMillis(10))
+                .build();
+        assertEquals(effect, other);
+    }
+
+    @Test
     public void testStartWaveformNotEqualsDifferentNumberOfSteps() {
-        VibrationEffect other = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
-                .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
+        VibrationEffect other = VibrationEffect.startWaveform(targetAmplitude(0.5f))
+                .addSustain(Duration.ofMillis(10))
+                .addTransition(Duration.ofMillis(100), targetAmplitude(1))
                 .build();
         assertNotEquals(TEST_WAVEFORM_BUILT, other);
     }
@@ -613,32 +711,21 @@
     @Test
     public void testStartWaveformNotEqualsDifferentTypesOfStep() {
         VibrationEffect first = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .addTransition(Duration.ofMillis(10), targetAmplitude(0.5f))
                 .build();
-        VibrationEffect second = VibrationEffect.startWaveform()
-                .addRamp(/* amplitude= */ 0.5f, /* duration= */ 10)
+        VibrationEffect second = VibrationEffect.startWaveform(targetAmplitude(0.5f))
+                .addSustain(Duration.ofMillis(10))
                 .build();
         assertNotEquals(first, second);
     }
 
     @Test
-    public void testStartWaveformNotEqualsDifferentRepeatIndex() {
-        VibrationEffect first = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
-                .build(0);
-        VibrationEffect second = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 1f, /* duration= */ 10)
-                .build(-1);
-        assertNotEquals(first, second);
-    }
-
-    @Test
     public void testStartWaveformNotEqualsDifferentAmplitudes() {
         VibrationEffect first = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .addTransition(Duration.ofMillis(10), targetAmplitude(0.5f))
                 .build();
         VibrationEffect second = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 1f, /* duration= */ 10)
+                .addTransition(Duration.ofMillis(10), targetAmplitude(0.8f))
                 .build();
         assertNotEquals(first, second);
     }
@@ -646,10 +733,10 @@
     @Test
     public void testStartWaveformNotEqualsDifferentFrequency() {
         VibrationEffect first = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* frequency= */ 1f, /* duration= */ 10)
+                .addTransition(Duration.ofMillis(10), targetAmplitude(0.5f), targetFrequency(100f))
                 .build();
         VibrationEffect second = VibrationEffect.startWaveform()
-                .addStep(/* amplitude= */ 0.5f, /* frequency= */ 50f, /* duration= */ 10)
+                .addTransition(Duration.ofMillis(10), targetAmplitude(0.5f), targetFrequency(50f))
                 .build();
         assertNotEquals(first, second);
     }
@@ -657,10 +744,10 @@
     @Test
     public void testStartWaveformNotEqualsDifferentDuration() {
         VibrationEffect first = VibrationEffect.startWaveform()
-                .addRamp(/* amplitude= */ 0.5f, /* duration= */ 1)
+                .addTransition(Duration.ofMillis(10), targetAmplitude(0.5f), targetFrequency(50f))
                 .build();
         VibrationEffect second = VibrationEffect.startWaveform()
-                .addRamp(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .addTransition(Duration.ofMillis(100), targetAmplitude(0.5f), targetFrequency(50f))
                 .build();
         assertNotEquals(first, second);
     }
@@ -670,23 +757,29 @@
         VibrationEffect.startWaveform().build();
     }
 
-    @Test
-    public void testStartWaveformFailsRepeatIndexOutOfBounds() {
-        assertThrows(IllegalArgumentException.class,
-                () -> VibrationEffect.startWaveform()
-                        .addStep(/* amplitude= */1, /* duration= */ 20)
-                        .build(-2));
+    @Test(expected = IllegalArgumentException.class)
+    public void testStartWaveformAddZeroDurationSustainIsInvalid() {
+        VibrationEffect.startWaveform().addSustain(Duration.ofNanos(1));
+    }
 
-        assertThrows(IllegalArgumentException.class,
-                () -> VibrationEffect.startWaveform()
-                        .addStep(/* amplitude= */1, /* duration= */ 20)
-                        .build(1));
+    @Test(expected = IllegalArgumentException.class)
+    public void testStartWaveformTransitionWithSameParameterTwiceIsInvalid() {
+        VibrationEffect.startWaveform().addTransition(Duration.ofSeconds(1),
+                targetAmplitude(0.8f), targetAmplitude(1f));
+    }
+
+    @Test
+    public void testStartWaveformZeroAmplitudeSustainIsSameAsOffPeriodOnlyComposition() {
+        assertEquals(
+                VibrationEffect.startWaveform().addSustain(Duration.ofMillis(1_000)).build(),
+                VibrationEffect.startComposition().addOffDuration(Duration.ofSeconds(1)).compose());
     }
 
     @Test
     public void testToString() {
         TEST_ONE_SHOT.toString();
         TEST_WAVEFORM.toString();
+        TEST_WAVEFORM_BUILT.toString();
         TEST_PREBAKED.toString();
         TEST_COMPOSED.toString();
     }
diff --git a/tests/tests/os/src/android/os/cts/VibratorManagerTest.java b/tests/tests/os/src/android/os/cts/VibratorManagerTest.java
index f0fb42e..75275d6 100644
--- a/tests/tests/os/src/android/os/cts/VibratorManagerTest.java
+++ b/tests/tests/os/src/android/os/cts/VibratorManagerTest.java
@@ -16,6 +16,9 @@
 
 package android.os.cts;
 
+import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
+import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -52,6 +55,7 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.time.Duration;
 import java.util.Arrays;
 
 @RunWith(AndroidJUnit4.class)
@@ -73,7 +77,7 @@
 
     private static final float MINIMUM_ACCEPTED_MEASUREMENT_INTERVAL_FREQUENCY = 1f;
     private static final float MINIMUM_ACCEPTED_FREQUENCY = 1f;
-    private static final float MAXIMUM_ACCEPTED_FREQUENCY = 2_000f;
+    private static final float MAXIMUM_ACCEPTED_FREQUENCY = 1_000f;
 
     private static final long CALLBACK_TIMEOUT_MILLIS = 5_000;
     private static final VibrationAttributes VIBRATION_ATTRIBUTES =
@@ -195,6 +199,51 @@
         assertStopsVibrating();
     }
 
+
+    @LargeTest
+    @Test
+    public void testVibrateWaveformWithFrequencyStartsAndFinishesVibration() {
+        int[] vibratorIds = mVibratorManager.getVibratorIds();
+        for (int vibratorId : vibratorIds) {
+            Vibrator vibrator = mVibratorManager.getVibrator(vibratorId);
+            if (!vibrator.hasFrequencyControl()) {
+                continue;
+            }
+            VibratorFrequencyProfile frequencyProfile = vibrator.getFrequencyProfile();
+
+            float minFrequency = frequencyProfile.getMinFrequency();
+            float maxFrequency = frequencyProfile.getMaxFrequency();
+            float resonantFrequency = vibrator.getResonantFrequency();
+            float sustainFrequency = Float.isNaN(resonantFrequency)
+                    ? (maxFrequency + minFrequency) / 2
+                    : resonantFrequency;
+
+            // Then ramp to zero amplitude at fixed frequency.
+            VibrationEffect waveform =
+                    VibrationEffect.startWaveform(targetAmplitude(0), targetFrequency(minFrequency))
+                            // Ramp from min to max frequency and from zero to max amplitude.
+                            .addTransition(Duration.ofMillis(10),
+                                    targetAmplitude(1), targetFrequency(maxFrequency))
+                            // Ramp back to min frequency and zero amplitude.
+                            .addTransition(Duration.ofMillis(10),
+                                    targetAmplitude(0), targetFrequency(minFrequency))
+                            // Then sustain at a fixed frequency and half amplitude.
+                            .addTransition(Duration.ZERO,
+                                    targetAmplitude(0.5f), targetFrequency(sustainFrequency))
+                            .addSustain(Duration.ofMillis(20))
+                            // Ramp from min to max frequency and at max amplitude.
+                            .addTransition(Duration.ZERO,
+                                    targetAmplitude(1), targetFrequency(minFrequency))
+                            .addTransition(Duration.ofMillis(10), targetFrequency(maxFrequency))
+                            // Ramp from max to min amplitude at max frequency.
+                            .addTransition(Duration.ofMillis(10), targetAmplitude(0))
+                            .build();
+            vibrator.vibrate(waveform);
+            assertStartsVibrating(vibratorId);
+            assertStopsVibrating();
+        }
+    }
+
     @Test
     public void testVibrateSingleVibrator() {
         int[] vibratorIds = mVibratorManager.getVibratorIds();
diff --git a/tests/tests/os/src/android/os/cts/VibratorTest.java b/tests/tests/os/src/android/os/cts/VibratorTest.java
index 53fdd21..7df216c 100644
--- a/tests/tests/os/src/android/os/cts/VibratorTest.java
+++ b/tests/tests/os/src/android/os/cts/VibratorTest.java
@@ -16,6 +16,9 @@
 
 package android.os.cts;
 
+import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
+import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -54,6 +57,7 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executors;
@@ -77,7 +81,7 @@
 
     private static final float MINIMUM_ACCEPTED_MEASUREMENT_INTERVAL_FREQUENCY = 1f;
     private static final float MINIMUM_ACCEPTED_FREQUENCY = 1f;
-    private static final float MAXIMUM_ACCEPTED_FREQUENCY = 2_000f;
+    private static final float MAXIMUM_ACCEPTED_FREQUENCY = 1_000f;
 
     private static final AudioAttributes AUDIO_ATTRIBUTES =
             new AudioAttributes.Builder()
@@ -272,6 +276,46 @@
         assertStopsVibrating();
     }
 
+    @LargeTest
+    @Test
+    public void testVibrateWaveformWithFrequencyStartsAndFinishesVibration() {
+        assumeTrue(mVibrator.hasFrequencyControl());
+        VibratorFrequencyProfile frequencyProfile = mVibrator.getFrequencyProfile();
+        assumeNotNull(frequencyProfile);
+
+        float minFrequency = Math.max(1f, frequencyProfile.getMinFrequency());
+        float maxFrequency = frequencyProfile.getMaxFrequency();
+        float resonantFrequency = mVibrator.getResonantFrequency();
+        float sustainFrequency = Float.isNaN(resonantFrequency)
+                ? (maxFrequency - minFrequency) / 2
+                : resonantFrequency;
+
+        // Ramp from min to max frequency and from zero to max amplitude.
+        // Then ramp to a fixed frequency at max amplitude.
+        // Then ramp to zero amplitude at fixed frequency.
+        VibrationEffect waveform =
+                VibrationEffect.startWaveform(targetAmplitude(0), targetFrequency(minFrequency))
+                        // Ramp from min to max frequency and from zero to max amplitude.
+                        .addTransition(Duration.ofMillis(10),
+                                targetAmplitude(1), targetFrequency(maxFrequency))
+                        // Ramp back to min frequency and zero amplitude.
+                        .addTransition(Duration.ofMillis(10),
+                                targetAmplitude(0), targetFrequency(minFrequency))
+                        // Then sustain at a fixed frequency and half amplitude.
+                        .addTransition(Duration.ZERO,
+                                targetAmplitude(0.5f), targetFrequency(sustainFrequency))
+                        .addSustain(Duration.ofMillis(20))
+                        // Ramp from min to max frequency and at max amplitude.
+                        .addTransition(Duration.ZERO,
+                                targetAmplitude(1), targetFrequency(minFrequency))
+                        .addTransition(Duration.ofMillis(10), targetFrequency(maxFrequency))
+                        // Ramp from max to min amplitude at max frequency.
+                        .addTransition(Duration.ofMillis(10), targetAmplitude(0))
+                        .build();
+        mVibrator.vibrate(waveform);
+        assertStartsThenStopsVibrating(50);
+    }
+
     @Test
     public void testVibratePredefined() {
         int[] supported = mVibrator.areEffectsSupported(PREDEFINED_EFFECTS);