Merge "Fix composed VibrationEffect timing out"
diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
index eebd046..be90530 100644
--- a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
@@ -31,9 +31,9 @@
     public final VibratorController controller;
     public final VibrationEffect.Composed effect;
     public final int segmentIndex;
-    public final long previousStepVibratorOffTimeout;
 
     long mVibratorOnResult;
+    long mPendingVibratorOffDeadline;
     boolean mVibratorCompleteCallbackReceived;
 
     /**
@@ -43,19 +43,19 @@
      * @param controller         The vibrator that is playing the effect.
      * @param effect             The effect being played in this step.
      * @param index              The index of the next segment to be played by this step
-     * @param previousStepVibratorOffTimeout The time the vibrator is expected to complete any
+     * @param pendingVibratorOffDeadline The time the vibrator is expected to complete any
      *                           previous vibration and turn off. This is used to allow this step to
      *                           be triggered when the completion callback is received, and can
      *                           be used to play effects back-to-back.
      */
     AbstractVibratorStep(VibrationStepConductor conductor, long startTime,
             VibratorController controller, VibrationEffect.Composed effect, int index,
-            long previousStepVibratorOffTimeout) {
+            long pendingVibratorOffDeadline) {
         super(conductor, startTime);
         this.controller = controller;
         this.effect = effect;
         this.segmentIndex = index;
-        this.previousStepVibratorOffTimeout = previousStepVibratorOffTimeout;
+        mPendingVibratorOffDeadline = pendingVibratorOffDeadline;
     }
 
     public int getVibratorId() {
@@ -69,27 +69,57 @@
 
     @Override
     public boolean acceptVibratorCompleteCallback(int vibratorId) {
-        boolean isSameVibrator = controller.getVibratorInfo().getId() == vibratorId;
-        mVibratorCompleteCallbackReceived |= isSameVibrator;
+        if (getVibratorId() != vibratorId) {
+            return false;
+        }
+
         // Only activate this step if a timeout was set to wait for the vibration to complete,
         // otherwise we are waiting for the correct time to play the next step.
-        return isSameVibrator && (previousStepVibratorOffTimeout > SystemClock.uptimeMillis());
+        boolean shouldAcceptCallback = mPendingVibratorOffDeadline > SystemClock.uptimeMillis();
+        if (VibrationThread.DEBUG) {
+            Slog.d(VibrationThread.TAG,
+                    "Received completion callback from " + vibratorId
+                            + ", accepted = " + shouldAcceptCallback);
+        }
+
+        // The callback indicates this vibrator has stopped, reset the timeout.
+        mPendingVibratorOffDeadline = 0;
+        mVibratorCompleteCallbackReceived = true;
+        return shouldAcceptCallback;
     }
 
     @Override
     public List<Step> cancel() {
         return Arrays.asList(new CompleteEffectVibratorStep(conductor, SystemClock.uptimeMillis(),
-                /* cancelled= */ true, controller, previousStepVibratorOffTimeout));
+                /* cancelled= */ true, controller, mPendingVibratorOffDeadline));
     }
 
     @Override
     public void cancelImmediately() {
-        if (previousStepVibratorOffTimeout > SystemClock.uptimeMillis()) {
+        if (mPendingVibratorOffDeadline > SystemClock.uptimeMillis()) {
             // Vibrator might be running from previous steps, so turn it off while canceling.
             stopVibrating();
         }
     }
 
+    protected long handleVibratorOnResult(long vibratorOnResult) {
+        mVibratorOnResult = vibratorOnResult;
+        if (VibrationThread.DEBUG) {
+            Slog.d(VibrationThread.TAG,
+                    "Turned on vibrator " + getVibratorId() + ", result = " + mVibratorOnResult);
+        }
+        if (mVibratorOnResult > 0) {
+            // Vibrator was turned on by this step, with vibratorOnResult as the duration.
+            // Set an extra timeout to wait for the vibrator completion callback.
+            mPendingVibratorOffDeadline = SystemClock.uptimeMillis() + mVibratorOnResult
+                    + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
+        } else {
+            // Vibrator does not support the request or failed to turn on, reset callback deadline.
+            mPendingVibratorOffDeadline = 0;
+        }
+        return mVibratorOnResult;
+    }
+
     protected void stopVibrating() {
         if (VibrationThread.DEBUG) {
             Slog.d(VibrationThread.TAG,
@@ -97,6 +127,7 @@
         }
         controller.off();
         getVibration().stats().reportVibratorOff();
+        mPendingVibratorOffDeadline = 0;
     }
 
     protected void changeAmplitude(float amplitude) {
@@ -109,40 +140,29 @@
     }
 
     /**
-     * Return the {@link VibrationStepConductor#nextVibrateStep} with same timings, only jumping
-     * the segments.
-     */
-    protected List<Step> skipToNextSteps(int segmentsSkipped) {
-        return nextSteps(startTime, previousStepVibratorOffTimeout, segmentsSkipped);
-    }
-
-    /**
-     * Return the {@link VibrationStepConductor#nextVibrateStep} with same start and off timings
-     * calculated from {@link #getVibratorOnDuration()}, jumping all played segments.
-     *
-     * <p>This method has same behavior as {@link #skipToNextSteps(int)} when the vibrator
-     * result is non-positive, meaning the vibrator has either ignored or failed to turn on.
+     * Return the {@link VibrationStepConductor#nextVibrateStep} with start and off timings
+     * calculated from {@link #getVibratorOnDuration()} based on the current
+     * {@link SystemClock#uptimeMillis()} and jumping all played segments from the effect.
      */
     protected List<Step> nextSteps(int segmentsPlayed) {
-        if (mVibratorOnResult <= 0) {
-            // Vibration was not started, so just skip the played segments and keep timings.
-            return skipToNextSteps(segmentsPlayed);
+        // Schedule next steps to run right away.
+        long nextStartTime = SystemClock.uptimeMillis();
+        if (mVibratorOnResult > 0) {
+            // Vibrator was turned on by this step, with mVibratorOnResult as the duration.
+            // Schedule next steps for right after the vibration finishes.
+            nextStartTime += mVibratorOnResult;
         }
-        long nextStartTime = SystemClock.uptimeMillis() + mVibratorOnResult;
-        long nextVibratorOffTimeout =
-                nextStartTime + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
-        return nextSteps(nextStartTime, nextVibratorOffTimeout, segmentsPlayed);
+        return nextSteps(nextStartTime, segmentsPlayed);
     }
 
     /**
-     * Return the {@link VibrationStepConductor#nextVibrateStep} with given start and off timings,
-     * which might be calculated independently, jumping all played segments.
+     * Return the {@link VibrationStepConductor#nextVibrateStep} with given start time,
+     * which might be calculated independently, and jumping all played segments from the effect.
      *
-     * <p>This should be used when the vibrator on/off state is not responsible for the steps
-     * execution timings, e.g. while playing the vibrator amplitudes.
+     * <p>This should be used when the vibrator on/off state is not responsible for the step
+     * execution timing, e.g. while playing the vibrator amplitudes.
      */
-    protected List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout,
-            int segmentsPlayed) {
+    protected List<Step> nextSteps(long nextStartTime, int segmentsPlayed) {
         int nextSegmentIndex = segmentIndex + segmentsPlayed;
         int effectSize = effect.getSegments().size();
         int repeatIndex = effect.getRepeatIndex();
@@ -154,7 +174,7 @@
             nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize);
         }
         Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect,
-                nextSegmentIndex, vibratorOffTimeout);
+                nextSegmentIndex, mPendingVibratorOffDeadline);
         return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep);
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
index 8585e34..fb5140d 100644
--- a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
@@ -34,9 +34,9 @@
     private final boolean mCancelled;
 
     CompleteEffectVibratorStep(VibrationStepConductor conductor, long startTime, boolean cancelled,
-            VibratorController controller, long previousStepVibratorOffTimeout) {
+            VibratorController controller, long pendingVibratorOffDeadline) {
         super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
-                previousStepVibratorOffTimeout);
+                pendingVibratorOffDeadline);
         mCancelled = cancelled;
     }
 
@@ -73,10 +73,11 @@
                 return VibrationStepConductor.EMPTY_STEP_LIST;
             }
 
+            long now = SystemClock.uptimeMillis();
             float currentAmplitude = controller.getCurrentAmplitude();
             long remainingOnDuration =
-                    previousStepVibratorOffTimeout - VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT
-                            - SystemClock.uptimeMillis();
+                    mPendingVibratorOffDeadline - now
+                            - VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
             long rampDownDuration =
                     Math.min(remainingOnDuration,
                             conductor.vibrationSettings.getRampDownDuration());
@@ -89,8 +90,10 @@
                     stopVibrating();
                     return VibrationStepConductor.EMPTY_STEP_LIST;
                 } else {
+                    // Vibration is completing normally, turn off after the deadline in case we
+                    // don't receive the callback in time (callback also triggers it right away).
                     return Arrays.asList(new TurnOffVibratorStep(
-                            conductor, previousStepVibratorOffTimeout, controller));
+                            conductor, mPendingVibratorOffDeadline, controller));
                 }
             }
 
@@ -100,13 +103,18 @@
                                 + " from amplitude " + currentAmplitude
                                 + " for " + rampDownDuration + "ms");
             }
+
+            // If we are cancelling this vibration then make sure the vibrator will be turned off
+            // immediately after the ramp off duration. Otherwise, this is a planned ramp off for
+            // the remaining ON duration, then just propagate the mPendingVibratorOffDeadline so the
+            // turn off step will wait for the vibration completion callback and end gracefully.
+            long rampOffVibratorOffDeadline =
+                    mCancelled ? (now + rampDownDuration) : mPendingVibratorOffDeadline;
             float amplitudeDelta = currentAmplitude / (rampDownDuration / stepDownDuration);
             float amplitudeTarget = currentAmplitude - amplitudeDelta;
-            long newVibratorOffTimeout =
-                    mCancelled ? rampDownDuration : previousStepVibratorOffTimeout;
             return Arrays.asList(
                     new RampOffVibratorStep(conductor, startTime, amplitudeTarget, amplitudeDelta,
-                            controller, newVibratorOffTimeout));
+                            controller, rampOffVibratorOffDeadline));
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
index f8b9926..545ec5b 100644
--- a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
@@ -40,11 +40,11 @@
 
     ComposePrimitivesVibratorStep(VibrationStepConductor conductor, long startTime,
             VibratorController controller, VibrationEffect.Composed effect, int index,
-            long previousStepVibratorOffTimeout) {
+            long pendingVibratorOffDeadline) {
         // This step should wait for the last vibration to finish (with the timeout) and for the
         // intended step start time (to respect the effect delays).
-        super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
-                index, previousStepVibratorOffTimeout);
+        super(conductor, Math.max(startTime, pendingVibratorOffDeadline), controller, effect,
+                index, pendingVibratorOffDeadline);
     }
 
     @Override
@@ -60,18 +60,22 @@
             if (primitives.isEmpty()) {
                 Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePrimitivesStep: "
                         + effect.getSegments().get(segmentIndex));
-                return skipToNextSteps(/* segmentsSkipped= */ 1);
+                // Skip this step and play the next one right away.
+                return nextSteps(/* segmentsPlayed= */ 1);
             }
 
             if (VibrationThread.DEBUG) {
                 Slog.d(VibrationThread.TAG, "Compose " + primitives + " primitives on vibrator "
-                        + controller.getVibratorInfo().getId());
+                        + getVibratorId());
             }
+
             PrimitiveSegment[] primitivesArray =
                     primitives.toArray(new PrimitiveSegment[primitives.size()]);
-            mVibratorOnResult = controller.on(primitivesArray, getVibration().id);
-            getVibration().stats().reportComposePrimitives(mVibratorOnResult, primitivesArray);
+            long vibratorOnResult = controller.on(primitivesArray, getVibration().id);
+            handleVibratorOnResult(vibratorOnResult);
+            getVibration().stats().reportComposePrimitives(vibratorOnResult, primitivesArray);
 
+            // The next start and off times will be calculated from mVibratorOnResult.
             return nextSteps(/* segmentsPlayed= */ primitives.size());
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
index 81f52c9..8bfa2c3 100644
--- a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
@@ -41,11 +41,11 @@
 
     ComposePwleVibratorStep(VibrationStepConductor conductor, long startTime,
             VibratorController controller, VibrationEffect.Composed effect, int index,
-            long previousStepVibratorOffTimeout) {
+            long pendingVibratorOffDeadline) {
         // This step should wait for the last vibration to finish (with the timeout) and for the
         // intended step start time (to respect the effect delays).
-        super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
-                index, previousStepVibratorOffTimeout);
+        super(conductor, Math.max(startTime, pendingVibratorOffDeadline), controller, effect,
+                index, pendingVibratorOffDeadline);
     }
 
     @Override
@@ -61,7 +61,8 @@
             if (pwles.isEmpty()) {
                 Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePwleStep: "
                         + effect.getSegments().get(segmentIndex));
-                return skipToNextSteps(/* segmentsSkipped= */ 1);
+                // Skip this step and play the next one right away.
+                return nextSteps(/* segmentsPlayed= */ 1);
             }
 
             if (VibrationThread.DEBUG) {
@@ -69,9 +70,11 @@
                         + controller.getVibratorInfo().getId());
             }
             RampSegment[] pwlesArray = pwles.toArray(new RampSegment[pwles.size()]);
-            mVibratorOnResult = controller.on(pwlesArray, getVibration().id);
-            getVibration().stats().reportComposePwle(mVibratorOnResult, pwlesArray);
+            long vibratorOnResult = controller.on(pwlesArray, getVibration().id);
+            handleVibratorOnResult(vibratorOnResult);
+            getVibration().stats().reportComposePwle(vibratorOnResult, pwlesArray);
 
+            // The next start and off times will be calculated from mVibratorOnResult.
             return nextSteps(/* segmentsPlayed= */ pwles.size());
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
index 419021478..d91bafa 100644
--- a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
@@ -35,11 +35,11 @@
 
     PerformPrebakedVibratorStep(VibrationStepConductor conductor, long startTime,
             VibratorController controller, VibrationEffect.Composed effect, int index,
-            long previousStepVibratorOffTimeout) {
+            long pendingVibratorOffDeadline) {
         // This step should wait for the last vibration to finish (with the timeout) and for the
         // intended step start time (to respect the effect delays).
-        super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
-                index, previousStepVibratorOffTimeout);
+        super(conductor, Math.max(startTime, pendingVibratorOffDeadline), controller, effect,
+                index, pendingVibratorOffDeadline);
     }
 
     @Override
@@ -50,7 +50,8 @@
             if (!(segment instanceof PrebakedSegment)) {
                 Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a "
                         + "PerformPrebakedVibratorStep: " + segment);
-                return skipToNextSteps(/* segmentsSkipped= */ 1);
+                // Skip this step and play the next one right away.
+                return nextSteps(/* segmentsPlayed= */ 1);
             }
 
             PrebakedSegment prebaked = (PrebakedSegment) segment;
@@ -61,10 +62,11 @@
             }
 
             VibrationEffect fallback = getVibration().getFallback(prebaked.getEffectId());
-            mVibratorOnResult = controller.on(prebaked, getVibration().id);
-            getVibration().stats().reportPerformEffect(mVibratorOnResult, prebaked);
+            long vibratorOnResult = controller.on(prebaked, getVibration().id);
+            handleVibratorOnResult(vibratorOnResult);
+            getVibration().stats().reportPerformEffect(vibratorOnResult, prebaked);
 
-            if (mVibratorOnResult == 0 && prebaked.shouldFallback()
+            if (vibratorOnResult == 0 && prebaked.shouldFallback()
                     && (fallback instanceof VibrationEffect.Composed)) {
                 if (VibrationThread.DEBUG) {
                     Slog.d(VibrationThread.TAG, "Playing fallback for effect "
@@ -72,14 +74,15 @@
                 }
                 AbstractVibratorStep fallbackStep = conductor.nextVibrateStep(startTime, controller,
                         replaceCurrentSegment((VibrationEffect.Composed) fallback),
-                        segmentIndex, previousStepVibratorOffTimeout);
+                        segmentIndex, mPendingVibratorOffDeadline);
                 List<Step> fallbackResult = fallbackStep.play();
                 // Update the result with the fallback result so this step is seamlessly
                 // replaced by the fallback to any outer application of this.
-                mVibratorOnResult = fallbackStep.getVibratorOnDuration();
+                handleVibratorOnResult(fallbackStep.getVibratorOnDuration());
                 return fallbackResult;
             }
 
+            // The next start and off times will be calculated from mVibratorOnResult.
             return nextSteps(/* segmentsPlayed= */ 1);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
diff --git a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
index 8cf5fb3..84da9f2 100644
--- a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
@@ -30,9 +30,9 @@
 
     RampOffVibratorStep(VibrationStepConductor conductor, long startTime, float amplitudeTarget,
             float amplitudeDelta, VibratorController controller,
-            long previousStepVibratorOffTimeout) {
+            long pendingVibratorOffDeadline) {
         super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
-                previousStepVibratorOffTimeout);
+                pendingVibratorOffDeadline);
         mAmplitudeTarget = amplitudeTarget;
         mAmplitudeDelta = amplitudeDelta;
     }
@@ -68,15 +68,17 @@
 
             float newAmplitudeTarget = mAmplitudeTarget - mAmplitudeDelta;
             if (newAmplitudeTarget < VibrationStepConductor.RAMP_OFF_AMPLITUDE_MIN) {
-                // Vibrator amplitude cannot go further down, just turn it off.
+                // Vibrator amplitude cannot go further down, just turn it off with the configured
+                // deadline that has been adjusted for the scenario when this was triggered by a
+                // cancelled vibration.
                 return Arrays.asList(new TurnOffVibratorStep(
-                        conductor, previousStepVibratorOffTimeout, controller));
+                        conductor, mPendingVibratorOffDeadline, controller));
             }
             return Arrays.asList(new RampOffVibratorStep(
                     conductor,
                     startTime + conductor.vibrationSettings.getRampStepDuration(),
                     newAmplitudeTarget, mAmplitudeDelta, controller,
-                    previousStepVibratorOffTimeout));
+                    mPendingVibratorOffDeadline));
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
index 6fb9111..1672470 100644
--- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
@@ -39,26 +39,34 @@
      */
     private static final int REPEATING_EFFECT_ON_DURATION = 5000; // 5s
 
-    private long mNextOffTime;
-
     SetAmplitudeVibratorStep(VibrationStepConductor conductor, long startTime,
             VibratorController controller, VibrationEffect.Composed effect, int index,
-            long previousStepVibratorOffTimeout) {
+            long pendingVibratorOffDeadline) {
         // This step has a fixed startTime coming from the timings of the waveform it's playing.
-        super(conductor, startTime, controller, effect, index, previousStepVibratorOffTimeout);
-        mNextOffTime = previousStepVibratorOffTimeout;
+        super(conductor, startTime, controller, effect, index, pendingVibratorOffDeadline);
     }
 
     @Override
     public boolean acceptVibratorCompleteCallback(int vibratorId) {
-        if (controller.getVibratorInfo().getId() == vibratorId) {
-            mVibratorCompleteCallbackReceived = true;
-            mNextOffTime = SystemClock.uptimeMillis();
+        // Ensure the super method is called and will reset the off timeout and boolean flag.
+        // This is true if the vibrator was ON and this callback has the same vibratorId.
+        if (!super.acceptVibratorCompleteCallback(vibratorId)) {
+            return false;
         }
+
         // Timings are tightly controlled here, so only trigger this step if the vibrator was
         // supposed to be ON but has completed prematurely, to turn it back on as soon as
-        // possible.
-        return mNextOffTime < startTime && controller.getCurrentAmplitude() > 0;
+        // possible. If the vibrator turned off during a zero-amplitude step, just wait for
+        // the correct start time of this step before playing it.
+        boolean shouldAcceptCallback =
+                (SystemClock.uptimeMillis() < startTime) && (controller.getCurrentAmplitude() > 0);
+
+        if (VibrationThread.DEBUG) {
+            Slog.d(VibrationThread.TAG,
+                    "Amplitude step received completion callback from " + vibratorId
+                            + ", accepted = " + shouldAcceptCallback);
+        }
+        return shouldAcceptCallback;
     }
 
     @Override
@@ -78,40 +86,38 @@
             if (mVibratorCompleteCallbackReceived && latency < 0) {
                 // This step was run early because the vibrator turned off prematurely.
                 // Turn it back on and return this same step to run at the exact right time.
-                mNextOffTime = turnVibratorBackOn(/* remainingDuration= */ -latency);
+                turnVibratorBackOn(/* remainingDuration= */ -latency);
                 return Arrays.asList(new SetAmplitudeVibratorStep(conductor, startTime, controller,
-                        effect, segmentIndex, mNextOffTime));
+                        effect, segmentIndex, mPendingVibratorOffDeadline));
             }
 
             VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
             if (!(segment instanceof StepSegment)) {
                 Slog.w(VibrationThread.TAG,
                         "Ignoring wrong segment for a SetAmplitudeVibratorStep: " + segment);
-                return skipToNextSteps(/* segmentsSkipped= */ 1);
+                // Use original startTime to avoid propagating latencies to the waveform.
+                return nextSteps(startTime, /* segmentsPlayed= */ 1);
             }
 
             StepSegment stepSegment = (StepSegment) segment;
             if (stepSegment.getDuration() == 0) {
-                // Skip waveform entries with zero timing.
-                return skipToNextSteps(/* segmentsSkipped= */ 1);
+                // Use original startTime to avoid propagating latencies to the waveform.
+                return nextSteps(startTime, /* segmentsPlayed= */ 1);
             }
 
             float amplitude = stepSegment.getAmplitude();
             if (amplitude == 0) {
-                if (previousStepVibratorOffTimeout > now) {
+                if (mPendingVibratorOffDeadline > now) {
                     // Amplitude cannot be set to zero, so stop the vibrator.
                     stopVibrating();
-                    mNextOffTime = now;
                 }
             } else {
-                if (startTime >= mNextOffTime) {
+                if (startTime >= mPendingVibratorOffDeadline) {
                     // Vibrator is OFF. Turn vibrator back on for the duration of another
                     // cycle before setting the amplitude.
                     long onDuration = getVibratorOnDuration(effect, segmentIndex);
                     if (onDuration > 0) {
-                        mVibratorOnResult = startVibrating(onDuration);
-                        mNextOffTime = now + onDuration
-                                + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
+                        startVibrating(onDuration);
                     }
                 }
                 changeAmplitude(amplitude);
@@ -119,27 +125,32 @@
 
             // Use original startTime to avoid propagating latencies to the waveform.
             long nextStartTime = startTime + segment.getDuration();
-            return nextSteps(nextStartTime, mNextOffTime, /* segmentsPlayed= */ 1);
+            return nextSteps(nextStartTime, /* segmentsPlayed= */ 1);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
     }
 
-    private long turnVibratorBackOn(long remainingDuration) {
+    private void turnVibratorBackOn(long remainingDuration) {
         long onDuration = getVibratorOnDuration(effect, segmentIndex);
         if (onDuration <= 0) {
             // Vibrator is supposed to go back off when this step starts, so just leave it off.
-            return previousStepVibratorOffTimeout;
+            return;
         }
         onDuration += remainingDuration;
+
+        if (VibrationThread.DEBUG) {
+            Slog.d(VibrationThread.TAG,
+                    "Turning the vibrator back ON using the remaining duration of "
+                            + remainingDuration + "ms, for a total of " + onDuration + "ms");
+        }
+
         float expectedAmplitude = controller.getCurrentAmplitude();
-        mVibratorOnResult = startVibrating(onDuration);
-        if (mVibratorOnResult > 0) {
+        long vibratorOnResult = startVibrating(onDuration);
+        if (vibratorOnResult > 0) {
             // Set the amplitude back to the value it was supposed to be playing at.
             changeAmplitude(expectedAmplitude);
         }
-        return SystemClock.uptimeMillis() + onDuration
-                + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
     }
 
     private long startVibrating(long duration) {
@@ -149,6 +160,7 @@
                             + duration + "ms");
         }
         long vibratorOnResult = controller.on(duration, getVibration().id);
+        handleVibratorOnResult(vibratorOnResult);
         getVibration().stats().reportVibratorOn(vibratorOnResult);
         return vibratorOnResult;
     }
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 0799b95..0af1718 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -112,8 +112,7 @@
 
     @Nullable
     AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller,
-            VibrationEffect.Composed effect, int segmentIndex,
-            long previousStepVibratorOffTimeout) {
+            VibrationEffect.Composed effect, int segmentIndex, long pendingVibratorOffDeadline) {
         if (Build.IS_DEBUGGABLE) {
             expectIsVibrationThread(true);
         }
@@ -123,24 +122,24 @@
         if (segmentIndex < 0) {
             // No more segments to play, last step is to complete the vibration on this vibrator.
             return new CompleteEffectVibratorStep(this, startTime, /* cancelled= */ false,
-                    controller, previousStepVibratorOffTimeout);
+                    controller, pendingVibratorOffDeadline);
         }
 
         VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
         if (segment instanceof PrebakedSegment) {
             return new PerformPrebakedVibratorStep(this, startTime, controller, effect,
-                    segmentIndex, previousStepVibratorOffTimeout);
+                    segmentIndex, pendingVibratorOffDeadline);
         }
         if (segment instanceof PrimitiveSegment) {
             return new ComposePrimitivesVibratorStep(this, startTime, controller, effect,
-                    segmentIndex, previousStepVibratorOffTimeout);
+                    segmentIndex, pendingVibratorOffDeadline);
         }
         if (segment instanceof RampSegment) {
             return new ComposePwleVibratorStep(this, startTime, controller, effect, segmentIndex,
-                    previousStepVibratorOffTimeout);
+                    pendingVibratorOffDeadline);
         }
         return new SetAmplitudeVibratorStep(this, startTime, controller, effect, segmentIndex,
-                previousStepVibratorOffTimeout);
+                pendingVibratorOffDeadline);
     }
 
     /** Called when this conductor is going to be started running by the VibrationThread. */
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 2f12a82..d1cde60 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -387,8 +387,8 @@
      * An internal-only version of vibrate that allows the caller access to the {@link Vibration}.
      * The Vibration is only returned if it is ongoing after this method returns.
      */
-    @Nullable
     @VisibleForTesting
+    @Nullable
     Vibration vibrateInternal(int uid, String opPkg, @NonNull CombinedVibration effect,
             @Nullable VibrationAttributes attrs, String reason, IBinder token) {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason);
@@ -1844,6 +1844,8 @@
                     attrs, commonOptions.description, deathBinder);
             if (vib != null && !commonOptions.background) {
                 try {
+                    // Waits for the client vibration to finish, but the VibrationThread may still
+                    // do cleanup after this.
                     vib.waitForEnd();
                 } catch (InterruptedException e) {
                 }
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 ca162ef..efc240d3 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -574,7 +574,7 @@
     }
 
     @Test
-    public void vibrate_singleVibratorComposedAndNoCapability_ignoresVibration() throws Exception {
+    public void vibrate_singleVibratorComposedAndNoCapability_ignoresVibration() {
         long vibrationId = 1;
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
@@ -666,6 +666,47 @@
     }
 
     @Test
+    public void vibrate_singleVibratorComposedWithFallback_replacedInTheMiddleOfComposition() {
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        fakeVibrator.setSupportedPrimitives(
+                VibrationEffect.Composition.PRIMITIVE_CLICK,
+                VibrationEffect.Composition.PRIMITIVE_TICK);
+        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+
+        long vibrationId = 1;
+        VibrationEffect fallback = VibrationEffect.createOneShot(10, 100);
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
+                .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_TICK))
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
+                .compose();
+        Vibration vib = createVibration(vibrationId, CombinedVibration.createParallel(effect));
+        vib.addFallback(VibrationEffect.EFFECT_TICK, fallback);
+        startThreadAndDispatcher(vib);
+        waitForCompletion();
+
+        // Use first duration the vibrator is turned on since we cannot estimate the clicks.
+        verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
+        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
+
+        List<VibrationEffectSegment> segments =
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId);
+        assertTrue("Wrong segments: " + segments, segments.size() >= 4);
+        assertTrue(segments.get(0) instanceof PrebakedSegment);
+        assertTrue(segments.get(1) instanceof PrimitiveSegment);
+        for (int i = 2; i < segments.size() - 1; i++) {
+            // One or more step segments as fallback for the EFFECT_TICK.
+            assertTrue(segments.get(i) instanceof StepSegment);
+        }
+        assertTrue(segments.get(segments.size() - 1) instanceof PrimitiveSegment);
+    }
+
+    @Test
     public void vibrate_singleVibratorPwle_runsComposePwle() throws Exception {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 36bec75..b8e1612 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -72,6 +72,7 @@
 import android.os.test.TestLooper;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
 import android.platform.test.annotations.Presubmit;
@@ -99,6 +100,7 @@
 
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -391,22 +393,22 @@
         IVibratorStateListener listenerMock = mockVibratorStateListener();
         service.registerVibratorStateListener(1, listenerMock);
 
-        vibrate(service, VibrationEffect.createOneShot(40, 100), ALARM_ATTRS);
-        // Wait until service knows vibrator is on.
-        assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
-        // Wait until effect ends.
-        assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+        long oneShotDuration = 20;
+        vibrateAndWaitUntilFinished(service,
+                VibrationEffect.createOneShot(oneShotDuration, VibrationEffect.DEFAULT_AMPLITUDE),
+                ALARM_ATTRS);
 
         InOrder inOrderVerifier = inOrder(listenerMock);
         // First notification done when listener is registered.
         inOrderVerifier.verify(listenerMock).onVibrating(eq(false));
         inOrderVerifier.verify(listenerMock).onVibrating(eq(true));
-        inOrderVerifier.verify(listenerMock).onVibrating(eq(false));
+        // The last notification is after the vibration has completed.
+        inOrderVerifier.verify(listenerMock, timeout(TEST_TIMEOUT_MILLIS)).onVibrating(eq(false));
         inOrderVerifier.verifyNoMoreInteractions();
 
         InOrder batteryVerifier = inOrder(mBatteryStatsMock);
         batteryVerifier.verify(mBatteryStatsMock)
-                .noteVibratorOn(UID, 40 + mVibrationConfig.getRampDownDurationMs());
+                .noteVibratorOn(UID, oneShotDuration + mVibrationConfig.getRampDownDurationMs());
         batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID);
     }
 
@@ -577,22 +579,18 @@
 
         setRingerMode(AudioManager.RINGER_MODE_SILENT);
         VibratorManagerService service = createSystemReadyService();
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), RINGTONE_ATTRS);
-        // Wait before checking it never played.
-        assertFalse(waitUntil(s -> !fakeVibrator.getAllEffectSegments().isEmpty(),
-                service, /* timeout= */ 50));
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                RINGTONE_ATTRS);
 
         setRingerMode(AudioManager.RINGER_MODE_NORMAL);
         service = createSystemReadyService();
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK), RINGTONE_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 1,
-                service, TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(
+                service, VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK), RINGTONE_ATTRS);
 
         setRingerMode(AudioManager.RINGER_MODE_VIBRATE);
         service = createSystemReadyService();
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), RINGTONE_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 2,
-                service, TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(
+                service, VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), RINGTONE_ATTRS);
 
         assertEquals(
                 Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_HEAVY_CLICK),
@@ -607,27 +605,18 @@
         fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_CLICK,
                 VibrationEffect.EFFECT_HEAVY_CLICK, VibrationEffect.EFFECT_DOUBLE_CLICK);
         VibratorManagerService service = createSystemReadyService();
-        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
 
-        // The haptic feedback should be ignored in low power, but not the ringtone. The end
-        // of the test asserts which actual effects ended up playing.
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK), HAPTIC_FEEDBACK_ATTRS);
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), RINGTONE_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 1,
-                service, TEST_TIMEOUT_MILLIS));
-        // Allow the ringtone to complete, as the other vibrations won't cancel it.
-        assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK),
+                HAPTIC_FEEDBACK_ATTRS);
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                RINGTONE_ATTRS);
 
         mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK),
-                /* attrs= */ null);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 2,
-                service, TEST_TIMEOUT_MILLIS));
-
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK),
-                NOTIFICATION_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 3,
-                service, TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(service,
+                VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK), /* attrs= */ null);
+        vibrateAndWaitUntilFinished(service,
+                VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), NOTIFICATION_ATTRS);
 
         assertEquals(
                 Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK),
@@ -693,22 +682,17 @@
                 Vibrator.VIBRATION_INTENSITY_HIGH);
         VibratorManagerService service = createSystemReadyService();
 
-        VibrationAttributes enforceFreshAttrs = new VibrationAttributes.Builder()
+        VibrationAttributes notificationWithFreshAttrs = new VibrationAttributes.Builder()
                 .setUsage(VibrationAttributes.USAGE_NOTIFICATION)
                 .setFlags(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)
                 .build();
 
         setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
                 Vibrator.VIBRATION_INTENSITY_LOW);
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), NOTIFICATION_ATTRS);
-        // VibrationThread will start this vibration async, so wait before vibrating a second time.
-        assertTrue(waitUntil(s -> mVibratorProviders.get(0).getAllEffectSegments().size() > 0,
-                service, TEST_TIMEOUT_MILLIS));
-
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK), enforceFreshAttrs);
-        // VibrationThread will start this vibration async, so wait before checking.
-        assertTrue(waitUntil(s -> mVibratorProviders.get(0).getAllEffectSegments().size() > 1,
-                service, TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                NOTIFICATION_ATTRS);
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK),
+                notificationWithFreshAttrs);
 
         assertEquals(
                 Arrays.asList(
@@ -784,21 +768,22 @@
         vibrate(service, repeatingEffect, new VibrationAttributes.Builder().setUsage(
                 VibrationAttributes.USAGE_UNKNOWN).build());
 
-        // VibrationThread will start this vibration async, so wait before checking it started.
+        // VibrationThread will start this vibration async, wait until it has started.
         assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
                 service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), HAPTIC_FEEDBACK_ATTRS);
-
-        // Wait before checking it never played a second effect.
-        assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
-                service, /* timeout= */ 50));
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                HAPTIC_FEEDBACK_ATTRS);
 
         // The time estimate is recorded when the vibration starts, repeating vibrations
         // are capped at BATTERY_STATS_REPEATING_VIBRATION_DURATION (=5000).
         verify(mBatteryStatsMock).noteVibratorOn(UID, 5000);
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
+        // No segment played is the prebaked CLICK from the second vibration.
+        assertFalse(
+                mVibratorProviders.get(1).getAllEffectSegments().stream()
+                        .anyMatch(segment -> segment instanceof PrebakedSegment));
     }
 
     @Test
@@ -811,7 +796,7 @@
                 new long[]{10_000, 10_000}, new int[]{128, 255}, -1);
         vibrate(service, alarmEffect, ALARM_ATTRS);
 
-        // VibrationThread will start this vibration async, so wait before checking it started.
+        // VibrationThread will start this vibration async, wait until it has started.
         assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
                 service, TEST_TIMEOUT_MILLIS));
 
@@ -841,14 +826,15 @@
         assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
                 service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
-
-        // Wait before checking it never played a second effect.
-        assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
-                service, /* timeout= */ 50));
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                HAPTIC_FEEDBACK_ATTRS);
 
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
+        // The second vibration shouldn't have played any prebaked segment.
+        assertFalse(
+                mVibratorProviders.get(1).getAllEffectSegments().stream()
+                        .anyMatch(segment -> segment instanceof PrebakedSegment));
     }
 
     @Test
@@ -856,6 +842,7 @@
             throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
         VibratorManagerService service = createSystemReadyService();
 
         VibrationEffect effect = VibrationEffect.createWaveform(
@@ -866,14 +853,16 @@
         assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
                 service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, effect, RINGTONE_ATTRS);
-
-        // VibrationThread will start this vibration async, so wait before checking it started.
-        assertTrue(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
-                service, TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                RINGTONE_ATTRS);
 
         // The second vibration should have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(2)).noteVibratorOn(anyInt(), anyLong());
+        // One segment played is the prebaked CLICK from the second vibration.
+        assertEquals(1,
+                mVibratorProviders.get(1).getAllEffectSegments().stream()
+                        .filter(PrebakedSegment.class::isInstance)
+                        .count());
     }
 
     @Test
@@ -892,12 +881,10 @@
 
         CombinedVibration effect = CombinedVibration.createParallel(
                 VibrationEffect.createOneShot(10, 10));
-        vibrate(service, effect, ALARM_ATTRS);
-        verify(mIInputManagerMock).vibrateCombined(eq(1), eq(effect), any());
+        vibrateAndWaitUntilFinished(service, effect, ALARM_ATTRS);
 
-        // VibrationThread will start this vibration async, so wait before checking it never played.
-        assertFalse(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
-                service, /* timeout= */ 50));
+        verify(mIInputManagerMock).vibrateCombined(eq(1), eq(effect), any());
+        assertTrue(mVibratorProviders.get(1).getAllEffectSegments().isEmpty());
     }
 
     @Test
@@ -992,9 +979,7 @@
                         .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
                         .compose())
                 .combine();
-        vibrate(service, effect, ALARM_ATTRS);
-        assertTrue(waitUntil(s -> !fakeVibrator1.getAllEffectSegments().isEmpty(), service,
-                TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(service, effect, ALARM_ATTRS);
 
         verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2}));
         verify(mNativeWrapperMock).triggerSynced(anyLong());
@@ -1016,9 +1001,7 @@
                 .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                 .addVibrator(2, VibrationEffect.createOneShot(10, 100))
                 .combine();
-        vibrate(service, effect, ALARM_ATTRS);
-        assertTrue(waitUntil(s -> !fakeVibrator1.getAllEffectSegments().isEmpty(), service,
-                TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(service, effect, ALARM_ATTRS);
 
         verify(mNativeWrapperMock, never()).prepareSynced(any());
         verify(mNativeWrapperMock, never()).triggerSynced(anyLong());
@@ -1036,9 +1019,7 @@
                 .addVibrator(1, VibrationEffect.createOneShot(10, 50))
                 .addVibrator(2, VibrationEffect.createOneShot(10, 100))
                 .combine();
-        vibrate(service, effect, ALARM_ATTRS);
-        assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
-                service, TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(service, effect, ALARM_ATTRS);
 
         verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2}));
         verify(mNativeWrapperMock, never()).triggerSynced(anyLong());
@@ -1057,9 +1038,7 @@
                 .addVibrator(1, VibrationEffect.createOneShot(10, 50))
                 .addVibrator(2, VibrationEffect.createOneShot(10, 100))
                 .combine();
-        vibrate(service, effect, ALARM_ATTRS);
-        assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
-                service, TEST_TIMEOUT_MILLIS));
+        vibrateAndWaitUntilFinished(service, effect, ALARM_ATTRS);
 
         verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2}));
         verify(mNativeWrapperMock).triggerSynced(anyLong());
@@ -1096,28 +1075,21 @@
                 VibrationEffect.Composition.PRIMITIVE_TICK);
         VibratorManagerService service = createSystemReadyService();
 
-        vibrate(service, VibrationEffect.startComposition()
+        vibrateAndWaitUntilFinished(service, VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .compose(), HAPTIC_FEEDBACK_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 1,
-                service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, CombinedVibration.startSequential()
+        vibrateAndWaitUntilFinished(service, CombinedVibration.startSequential()
                 .addNext(1, VibrationEffect.createOneShot(100, 125))
                 .combine(), NOTIFICATION_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 2,
-                service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, VibrationEffect.startComposition()
+        vibrateAndWaitUntilFinished(service, VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                 .compose(), ALARM_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 3,
-                service, TEST_TIMEOUT_MILLIS));
 
         // Ring vibrations have intensity OFF and are not played.
-        vibrate(service, VibrationEffect.createOneShot(100, 125), RINGTONE_ATTRS);
-        assertFalse(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() > 3,
-                service, /* timeout= */ 50));
+        vibrateAndWaitUntilFinished(service, VibrationEffect.createOneShot(100, 125),
+                RINGTONE_ATTRS);
 
         // Only 3 effects played successfully.
         assertEquals(3, fakeVibrator.getAllEffectSegments().size());
@@ -1145,6 +1117,7 @@
                         .combine(),
                 HAPTIC_FEEDBACK_ATTRS);
 
+        // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
@@ -1159,14 +1132,50 @@
         VibratorManagerService service = createSystemReadyService();
 
         vibrate(service, VibrationEffect.createOneShot(1000, 100), HAPTIC_FEEDBACK_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         service.updateServiceState();
+
         // Vibration is not stopped nearly after updating service.
         assertFalse(waitUntil(s -> !s.isVibrating(1), service, 50));
     }
 
     @Test
+    public void vibrate_prebakedAndComposedVibrationsWithFallbacks_playsFallbackOnlyForPredefined()
+            throws Exception {
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        mVibratorProviders.get(1).setSupportedPrimitives(
+                VibrationEffect.Composition.PRIMITIVE_CLICK);
+
+        VibratorManagerService service = createSystemReadyService();
+        vibrateAndWaitUntilFinished(service,
+                VibrationEffect.startComposition()
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .compose(),
+                ALARM_ATTRS);
+
+        List<VibrationEffectSegment> segments = mVibratorProviders.get(1).getAllEffectSegments();
+        // At least one step segment played as fallback for unusupported vibration effect
+        assertTrue(segments.size() > 2);
+        // 0: Supported effect played
+        assertTrue(segments.get(0) instanceof PrebakedSegment);
+        // 1: No segment for unsupported primitive
+        // 2: One or more intermediate step segments as fallback for unsupported effect
+        for (int i = 1; i < segments.size() - 1; i++) {
+            assertTrue(segments.get(i) instanceof StepSegment);
+        }
+        // 3: Supported primitive played
+        assertTrue(segments.get(segments.size() - 1) instanceof PrimitiveSegment);
+    }
+
+    @Test
     public void cancelVibrate_withoutUsageFilter_stopsVibrating() throws Exception {
         mockVibrators(1);
         VibratorManagerService service = createSystemReadyService();
@@ -1175,9 +1184,13 @@
         assertFalse(service.isVibrating(1));
 
         vibrate(service, VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100), ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service);
+
+        // Alarm cancelled on filter match all.
         assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
     }
 
@@ -1187,6 +1200,8 @@
         VibratorManagerService service = createSystemReadyService();
 
         vibrate(service, VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100), ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         // Vibration is not cancelled with a different usage.
@@ -1216,6 +1231,8 @@
         VibratorManagerService service = createSystemReadyService();
 
         vibrate(service, VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100), attrs);
+
+        // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         // Do not cancel UNKNOWN vibration when filter is being applied for other usages.
@@ -1232,6 +1249,8 @@
         assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         vibrate(service, VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100), attrs);
+
+        // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         // Cancel UNKNOWN vibration when all vibrations are being cancelled.
@@ -1312,6 +1331,8 @@
 
         VibrationEffect effect = VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100);
         vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ATTRS,