diff options
5 files changed, 255 insertions, 51 deletions
diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java index 3550bda282dd..12e68b10c3df 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 @@ abstract class AbstractVibratorStep extends Step { */ 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 d1ea80557419..3bc11c8f8322 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 @@ import java.util.List; * {@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 @@ final class ComposePrimitivesVibratorStep extends AbstractVibratorStep { // 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 @@ final class ComposePrimitivesVibratorStep extends AbstractVibratorStep { 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 73bf933f8bc9..919f1be27ef9 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 @@ import java.util.List; * {@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 @@ final class ComposePwleVibratorStep extends AbstractVibratorStep { // 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 @@ final class ComposePwleVibratorStep extends AbstractVibratorStep { 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 d5c11161bdfa..1f0d2d71d25c 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 @@ import java.util.List; * 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 @@ final class SetAmplitudeVibratorStep extends AbstractVibratorStep { 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 9f135918daa2..de5f6ed2ae5d 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 @@ public class VibrationThreadTest { } @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 @@ public class VibrationThreadTest { 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 @@ public class VibrationThreadTest { fakeVibrator.getEffectSegments(vibrationId)); } - + @LargeTest @Test public void vibrate_singleVibratorRepeatingAlwaysOnWaveform_turnsVibratorBackOn() throws Exception { @@ -329,22 +389,21 @@ public class VibrationThreadTest { 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 @@ public class VibrationThreadTest { @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 @@ public class VibrationThreadTest { .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 @@ public class VibrationThreadTest { // 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 @@ public class VibrationThreadTest { 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 @@ public class VibrationThreadTest { } @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 |