Merge "Use composition size limit on repeating effects" into tm-dev am: 48a15efcc7 am: 1a9e68638d

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17474926

Change-Id: I07311446e1ec39e3a37f4c25150816a2dc956c1e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
index 3550bda..12e68b1 100644
--- a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
@@ -141,8 +141,16 @@
      */
     protected List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout,
             int segmentsPlayed) {
+        int nextSegmentIndex = segmentIndex + segmentsPlayed;
+        int effectSize = effect.getSegments().size();
+        int repeatIndex = effect.getRepeatIndex();
+        if (nextSegmentIndex >= effectSize && repeatIndex >= 0) {
+            // Count the loops that were played.
+            int loopSize = effectSize - repeatIndex;
+            nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize);
+        }
         Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect,
-                segmentIndex + segmentsPlayed, vibratorOffTimeout);
+                nextSegmentIndex, vibratorOffTimeout);
         return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep);
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
index d1ea805..3bc11c8 100644
--- a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
@@ -32,6 +32,11 @@
  * {@link PrimitiveSegment} starting at the current index.
  */
 final class ComposePrimitivesVibratorStep extends AbstractVibratorStep {
+    /**
+     * Default limit to the number of primitives in a composition, if none is defined by the HAL,
+     * to prevent repeating effects from generating an infinite list.
+     */
+    private static final int DEFAULT_COMPOSITION_SIZE_LIMIT = 100;
 
     ComposePrimitivesVibratorStep(VibrationStepConductor conductor, long startTime,
             VibratorController controller, VibrationEffect.Composed effect, int index,
@@ -49,18 +54,8 @@
             // Load the next PrimitiveSegments to create a single compose call to the vibrator,
             // limited to the vibrator composition maximum size.
             int limit = controller.getVibratorInfo().getCompositionSizeMax();
-            int segmentCount = limit > 0
-                    ? Math.min(effect.getSegments().size(), segmentIndex + limit)
-                    : effect.getSegments().size();
-            List<PrimitiveSegment> primitives = new ArrayList<>();
-            for (int i = segmentIndex; i < segmentCount; i++) {
-                VibrationEffectSegment segment = effect.getSegments().get(i);
-                if (segment instanceof PrimitiveSegment) {
-                    primitives.add((PrimitiveSegment) segment);
-                } else {
-                    break;
-                }
-            }
+            List<PrimitiveSegment> primitives = unrollPrimitiveSegments(effect, segmentIndex,
+                    limit > 0 ? limit : DEFAULT_COMPOSITION_SIZE_LIMIT);
 
             if (primitives.isEmpty()) {
                 Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePrimitivesStep: "
@@ -81,4 +76,44 @@
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
     }
+
+    /**
+     * Get the primitive segments to be played by this step as a single composition, starting at
+     * {@code startIndex} until:
+     *
+     * <ol>
+     *     <li>There are no more segments in the effect;
+     *     <li>The first non-primitive segment is found;
+     *     <li>The given limit to the composition size is reached.
+     * </ol>
+     *
+     * <p>If the effect is repeating then this method will generate the largest composition within
+     * given limit.
+     */
+    private List<PrimitiveSegment> unrollPrimitiveSegments(VibrationEffect.Composed effect,
+            int startIndex, int limit) {
+        List<PrimitiveSegment> segments = new ArrayList<>(limit);
+        int segmentCount = effect.getSegments().size();
+        int repeatIndex = effect.getRepeatIndex();
+
+        for (int i = startIndex; segments.size() < limit; i++) {
+            if (i == segmentCount) {
+                if (repeatIndex >= 0) {
+                    i = repeatIndex;
+                } else {
+                    // Non-repeating effect, stop collecting primitives.
+                    break;
+                }
+            }
+            VibrationEffectSegment segment = effect.getSegments().get(i);
+            if (segment instanceof PrimitiveSegment) {
+                segments.add((PrimitiveSegment) segment);
+            } else {
+                // First non-primitive segment, stop collecting primitives.
+                break;
+            }
+        }
+
+        return segments;
+    }
 }
diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
index 73bf933..919f1be 100644
--- a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
@@ -33,6 +33,11 @@
  * {@link StepSegment} or {@link RampSegment} starting at the current index.
  */
 final class ComposePwleVibratorStep extends AbstractVibratorStep {
+    /**
+     * Default limit to the number of PWLE segments, if none is defined by the HAL, to prevent
+     * repeating effects from generating an infinite list.
+     */
+    private static final int DEFAULT_PWLE_SIZE_LIMIT = 100;
 
     ComposePwleVibratorStep(VibrationStepConductor conductor, long startTime,
             VibratorController controller, VibrationEffect.Composed effect, int index,
@@ -50,18 +55,8 @@
             // Load the next RampSegments to create a single composePwle call to the vibrator,
             // limited to the vibrator PWLE maximum size.
             int limit = controller.getVibratorInfo().getPwleSizeMax();
-            int segmentCount = limit > 0
-                    ? Math.min(effect.getSegments().size(), segmentIndex + limit)
-                    : effect.getSegments().size();
-            List<RampSegment> pwles = new ArrayList<>();
-            for (int i = segmentIndex; i < segmentCount; i++) {
-                VibrationEffectSegment segment = effect.getSegments().get(i);
-                if (segment instanceof RampSegment) {
-                    pwles.add((RampSegment) segment);
-                } else {
-                    break;
-                }
-            }
+            List<RampSegment> pwles = unrollRampSegments(effect, segmentIndex,
+                    limit > 0 ? limit : DEFAULT_PWLE_SIZE_LIMIT);
 
             if (pwles.isEmpty()) {
                 Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePwleStep: "
@@ -81,4 +76,88 @@
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
     }
+
+    /**
+     * Get the ramp segments to be played by this step for a waveform, starting at
+     * {@code startIndex} until:
+     *
+     * <ol>
+     *     <li>There are no more segments in the effect;
+     *     <li>The first non-ramp segment is found;
+     *     <li>The given limit to the PWLE size is reached.
+     * </ol>
+     *
+     * <p>If the effect is repeating then this method will generate the largest PWLE within given
+     * limit. This will also optimize to end the list at a ramp to zero-amplitude, if possible, and
+     * avoid braking down the effect in non-zero amplitude.
+     */
+    private List<RampSegment> unrollRampSegments(VibrationEffect.Composed effect, int startIndex,
+            int limit) {
+        List<RampSegment> segments = new ArrayList<>(limit);
+        float bestBreakAmplitude = 1;
+        int bestBreakPosition = limit; // Exclusive index.
+
+        int segmentCount = effect.getSegments().size();
+        int repeatIndex = effect.getRepeatIndex();
+
+        // Loop once after reaching the limit to see if breaking it will really be necessary, then
+        // apply the best break position found, otherwise return the full list as it fits the limit.
+        for (int i = startIndex; segments.size() <= limit; i++) {
+            if (i == segmentCount) {
+                if (repeatIndex >= 0) {
+                    i = repeatIndex;
+                } else {
+                    // Non-repeating effect, stop collecting ramps.
+                    break;
+                }
+            }
+            VibrationEffectSegment segment = effect.getSegments().get(i);
+            if (segment instanceof RampSegment) {
+                RampSegment rampSegment = (RampSegment) segment;
+                segments.add(rampSegment);
+
+                if (isBetterBreakPosition(segments, bestBreakAmplitude, limit)) {
+                    // Mark this position as the best one so far to break a long waveform.
+                    bestBreakAmplitude = rampSegment.getEndAmplitude();
+                    bestBreakPosition = segments.size(); // Break after this ramp ends.
+                }
+            } else {
+                // First non-ramp segment, stop collecting ramps.
+                break;
+            }
+        }
+
+        return segments.size() > limit
+                // Remove excessive segments, using the best breaking position recorded.
+                ? segments.subList(0, bestBreakPosition)
+                // Return all collected ramp segments.
+                : segments;
+    }
+
+    /**
+     * Returns true if the current segment list represents a better break position for a PWLE,
+     * given the current amplitude being used for breaking it at a smaller size and the size limit.
+     */
+    private boolean isBetterBreakPosition(List<RampSegment> segments,
+            float currentBestBreakAmplitude, int limit) {
+        RampSegment lastSegment = segments.get(segments.size() - 1);
+        float breakAmplitudeCandidate = lastSegment.getEndAmplitude();
+        int breakPositionCandidate = segments.size();
+
+        if (breakPositionCandidate > limit) {
+            // We're beyond limit, last break position found should be used.
+            return false;
+        }
+        if (breakAmplitudeCandidate == 0) {
+            // Breaking at amplitude zero at any position is always preferable.
+            return true;
+        }
+        if (breakPositionCandidate < limit / 2) {
+            // Avoid breaking at the first half of the allowed maximum size, even if amplitudes are
+            // lower, to avoid creating PWLEs that are too small unless it's to break at zero.
+            return false;
+        }
+        // Prefer lower amplitudes at a later position for breaking the PWLE in a more subtle way.
+        return breakAmplitudeCandidate <= currentBestBreakAmplitude;
+    }
 }
diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
index d5c1116..1f0d2d7 100644
--- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
@@ -33,6 +33,12 @@
  * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}.
  */
 final class SetAmplitudeVibratorStep extends AbstractVibratorStep {
+    /**
+     * The repeating waveform keeps the vibrator ON all the time. Use a minimum duration to
+     * prevent short patterns from turning the vibrator ON too frequently.
+     */
+    private static final int REPEATING_EFFECT_ON_DURATION = 5000; // 5s
+
     private long mNextOffTime;
 
     SetAmplitudeVibratorStep(VibrationStepConductor conductor, long startTime,
@@ -170,10 +176,7 @@
                 repeatIndex = -1;
             }
             if (i == startIndex) {
-                // The repeating waveform keeps the vibrator ON all the time. Use a minimum
-                // of 1s duration to prevent short patterns from turning the vibrator ON too
-                // frequently.
-                return Math.max(timing, 1000);
+                return Math.max(timing, REPEATING_EFFECT_ON_DURATION);
             }
         }
         if (i == segmentCount && effect.getRepeatIndex() < 0) {
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 9f13591..de5f6ed 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -276,7 +276,7 @@
     }
 
     @Test
-    public void vibrate_singleVibratorRepeatingShortAlwaysOnWaveform_turnsVibratorOnForASecond()
+    public void vibrate_singleVibratorRepeatingShortAlwaysOnWaveform_turnsVibratorOnForLonger()
             throws Exception {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -293,11 +293,71 @@
 
         verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        assertEquals(Arrays.asList(expectedOneShot(1000)),
+        assertEquals(Arrays.asList(expectedOneShot(5000)),
                 fakeVibrator.getEffectSegments(vibrationId));
     }
 
     @Test
+    public void vibrate_singleVibratorRepeatingPwle_generatesLargestPwles() throws Exception {
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
+        fakeVibrator.setMinFrequency(100);
+        fakeVibrator.setResonantFrequency(150);
+        fakeVibrator.setFrequencyResolution(50);
+        fakeVibrator.setMaxAmplitudes(1, 1, 1);
+        fakeVibrator.setPwleSizeMax(10);
+
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.startWaveform(targetAmplitude(1))
+                // Very long segment so thread will be cancelled after first PWLE is triggered.
+                .addTransition(Duration.ofMillis(100), targetFrequency(100))
+                .build();
+        VibrationEffect repeatingEffect = VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(effect)
+                .compose();
+        VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, repeatingEffect);
+
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+                TEST_TIMEOUT_MILLIS));
+        conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false);
+        waitForCompletion();
+
+        // PWLE size max was used to generate a single vibrate call with 10 segments.
+        verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
+        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
+        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+    }
+
+    @Test
+    public void vibrate_singleVibratorRepeatingPrimitives_generatesLargestComposition()
+            throws Exception {
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        fakeVibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK);
+        fakeVibrator.setCompositionSizeMax(10);
+
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.startComposition()
+                // Very long delay so thread will be cancelled after first PWLE is triggered.
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
+                .compose();
+        VibrationEffect repeatingEffect = VibrationEffect.startComposition()
+                .repeatEffectIndefinitely(effect)
+                .compose();
+        VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, repeatingEffect);
+
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+                TEST_TIMEOUT_MILLIS));
+        conductor.notifyCancelled(Vibration.Status.CANCELLED_SUPERSEDED, /* immediate= */ false);
+        waitForCompletion();
+
+        // Composition size max was used to generate a single vibrate call with 10 primitives.
+        verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_SUPERSEDED);
+        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
+        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+    }
+
+    @Test
     public void vibrate_singleVibratorRepeatingLongAlwaysOnWaveform_turnsVibratorOnForACycle()
             throws Exception {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
@@ -319,7 +379,7 @@
                 fakeVibrator.getEffectSegments(vibrationId));
     }
 
-
+    @LargeTest
     @Test
     public void vibrate_singleVibratorRepeatingAlwaysOnWaveform_turnsVibratorBackOn()
             throws Exception {
@@ -329,22 +389,21 @@
         long vibrationId = 1;
         int[] amplitudes = new int[]{1, 2};
         VibrationEffect effect = VibrationEffect.createWaveform(
-                new long[]{900, 50}, amplitudes, 0);
+                new long[]{4900, 50}, amplitudes, 0);
         VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, effect);
 
-        assertTrue(waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2 * amplitudes.length,
-                1000 + TEST_TIMEOUT_MILLIS));
+        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() > 1,
+                5000 + TEST_TIMEOUT_MILLIS));
         conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false);
         waitForCompletion();
 
         verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        assertEquals(2, fakeVibrator.getEffectSegments(vibrationId).size());
-        // First time turn vibrator ON for minimum of 1s.
-        assertEquals(1000L, fakeVibrator.getEffectSegments(vibrationId).get(0).getDuration());
+        // First time turn vibrator ON for minimum of 5s.
+        assertEquals(5000L, fakeVibrator.getEffectSegments(vibrationId).get(0).getDuration());
         // Vibrator turns off in the middle of the second execution of first step, turn it back ON
-        // for another 1s + remaining of 850ms.
-        assertEquals(1850,
+        // for another 5s + remaining of 850ms.
+        assertEquals(4900 + 50 + 4900,
                 fakeVibrator.getEffectSegments(vibrationId).get(1).getDuration(), /* delta= */ 20);
         // Set amplitudes for a cycle {1, 2}, start second loop then turn it back on to same value.
         assertEquals(expectedAmplitudes(1, 2, 1, 1),
@@ -530,12 +589,18 @@
 
     @Test
     public void vibrate_singleVibratorComposedEffects_runsDifferentVibrations() throws Exception {
-        mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
-        mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives(
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        fakeVibrator.setSupportedPrimitives(
                 VibrationEffect.Composition.PRIMITIVE_CLICK,
                 VibrationEffect.Composition.PRIMITIVE_TICK);
-        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS,
-                IVibrator.CAP_AMPLITUDE_CONTROL);
+        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS,
+                IVibrator.CAP_COMPOSE_PWLE_EFFECTS, IVibrator.CAP_AMPLITUDE_CONTROL);
+        fakeVibrator.setMinFrequency(100);
+        fakeVibrator.setResonantFrequency(150);
+        fakeVibrator.setFrequencyResolution(50);
+        fakeVibrator.setMaxAmplitudes(
+                0.5f /* 100Hz*/, 1 /* 150Hz */, 0.6f /* 200Hz */);
 
         long vibrationId = 1;
         VibrationEffect effect = VibrationEffect.startComposition()
@@ -543,7 +608,11 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
-                .addOffDuration(Duration.ofMillis(100))
+                .addEffect(VibrationEffect.startWaveform()
+                        .addTransition(Duration.ofMillis(10),
+                                targetAmplitude(1), targetFrequency(100))
+                        .addTransition(Duration.ofMillis(20), targetFrequency(120))
+                        .build())
                 .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                 .compose();
         startThreadAndDispatcher(vibrationId, effect);
@@ -552,7 +621,7 @@
         // Use first duration the vibrator is turned on since we cannot estimate the clicks.
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(
@@ -560,6 +629,10 @@
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0),
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0),
                 expectedPrebaked(VibrationEffect.EFFECT_CLICK),
+                expectedRamp(/* startAmplitude= */ 0, /* endAmplitude= */ 0.5f,
+                        /* startFrequencyHz= */ 150, /* endFrequencyHz= */ 100, /* duration= */ 10),
+                expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.7f,
+                        /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 120, /* duration= */ 20),
                 expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
                 mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
@@ -605,30 +678,36 @@
     }
 
     @Test
-    public void vibrate_singleVibratorLargePwle_splitsVibratorComposeCalls() {
+    public void vibrate_singleVibratorLargePwle_splitsComposeCallWhenAmplitudeIsLowest() {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
         fakeVibrator.setMinFrequency(100);
         fakeVibrator.setResonantFrequency(150);
         fakeVibrator.setFrequencyResolution(50);
         fakeVibrator.setMaxAmplitudes(1, 1, 1);
-        fakeVibrator.setPwleSizeMax(2);
+        fakeVibrator.setPwleSizeMax(3);
 
         long vibrationId = 1;
         VibrationEffect effect = VibrationEffect.startWaveform(targetAmplitude(1))
                 .addSustain(Duration.ofMillis(10))
                 .addTransition(Duration.ofMillis(20), targetAmplitude(0))
+                // Waveform will be split here, after vibration goes to zero amplitude
                 .addTransition(Duration.ZERO, targetAmplitude(0.8f), targetFrequency(100))
                 .addSustain(Duration.ofMillis(30))
                 .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
+                // Waveform will be split here at lowest amplitude.
+                .addTransition(Duration.ofMillis(40), targetAmplitude(0.7f), targetFrequency(200))
+                .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
                 .build();
         startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion();
 
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
-        // Vibrator compose called twice.
-        verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        assertEquals(4, fakeVibrator.getEffectSegments(vibrationId).size());
+
+        // Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments.
+        // Using best split points instead of max-packing PWLEs.
+        verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        assertEquals(6, fakeVibrator.getEffectSegments(vibrationId).size());
     }
 
     @Test