diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index 3090e6d8c6224..dccf8c7858cc9 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -252,36 +252,39 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { } } - /** - * Get the duration the vibrator will be on for given {@code waveform}, starting at {@code - * startIndex} until the next time it's vibrating amplitude is zero. - */ - private static long getVibratorOnDuration(VibrationEffect.Composed effect, int startIndex) { - List segments = effect.getSegments(); - int segmentCount = segments.size(); - int repeatIndex = effect.getRepeatIndex(); - int i = startIndex; - long timing = 0; - while (i < segmentCount) { - if (!(segments.get(i) instanceof StepSegment)) { - break; - } - StepSegment stepSegment = (StepSegment) segments.get(i); - if (stepSegment.getAmplitude() == 0) { - break; - } - timing += stepSegment.getDuration(); - i++; - if (i == segmentCount && repeatIndex >= 0) { - i = repeatIndex; - // prevent infinite loop - repeatIndex = -1; - } - if (i == startIndex) { - return 1000; + @Nullable + private SingleVibratorStep nextVibrateStep(long startTime, VibratorController controller, + VibrationEffect.Composed effect, int segmentIndex, long vibratorOffTimeout) { + // Some steps should only start after the vibrator has finished the previous vibration, so + // make sure we take the latest between both timings. + long latestStartTime = Math.max(startTime, vibratorOffTimeout); + if (segmentIndex >= effect.getSegments().size()) { + segmentIndex = effect.getRepeatIndex(); + } + if (segmentIndex < 0) { + if (vibratorOffTimeout > SystemClock.uptimeMillis()) { + // No more segments to play, last step is to wait for the vibrator to complete + return new OffStep(vibratorOffTimeout, controller); + } else { + return null; } } - return timing; + + VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); + if (segment instanceof PrebakedSegment) { + return new PerformStep(latestStartTime, controller, effect, segmentIndex, + vibratorOffTimeout); + } + if (segment instanceof PrimitiveSegment) { + return new ComposePrimitivesStep(latestStartTime, controller, effect, segmentIndex, + vibratorOffTimeout); + } + if (segment instanceof RampSegment) { + // TODO(b/167947076): check capabilities to play steps with PWLE once APIs introduced + return new ComposePwleStep(latestStartTime, controller, effect, segmentIndex, + vibratorOffTimeout); + } + return new AmplitudeStep(startTime, controller, effect, segmentIndex, vibratorOffTimeout); } private static CombinedVibrationEffect.Sequential toSequential(CombinedVibrationEffect effect) { @@ -449,8 +452,13 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { duration = startVibrating(effectMapping, nextSteps); noteVibratorOn(duration); } finally { - // If this step triggered any vibrator then add a finish step to wait for all + if (duration < 0) { + // Something failed while playing this step so stop playing this sequence. + return EMPTY_STEP_LIST; + } + // It least one vibrator was started then add a finish step to wait for all // active vibrators to finish their individual steps before going to the next. + // Otherwise this step was ignored so just go to the next one. Step nextStep = duration > 0 ? new FinishVibrateStep(this) : nextStep(); if (nextStep != null) { nextSteps.add(nextStep); @@ -506,11 +514,13 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { return 0; } - VibratorOnStep[] steps = new VibratorOnStep[vibratorCount]; + SingleVibratorStep[] steps = new SingleVibratorStep[vibratorCount]; long vibrationStartTime = SystemClock.uptimeMillis(); for (int i = 0; i < vibratorCount; i++) { - steps[i] = new VibratorOnStep(vibrationStartTime, - mVibrators.get(effectMapping.vibratorIdAt(i)), effectMapping.effectAt(i)); + steps[i] = nextVibrateStep(vibrationStartTime, + mVibrators.get(effectMapping.vibratorIdAt(i)), + effectMapping.effectAt(i), + /* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0); } if (steps.length == 1) { @@ -524,35 +534,52 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { synchronized (mLock) { boolean hasPrepared = false; boolean hasTriggered = false; + long maxDuration = 0; try { hasPrepared = mCallbacks.prepareSyncedVibration( effectMapping.getRequiredSyncCapabilities(), effectMapping.getVibratorIds()); - long duration = 0; - for (VibratorOnStep step : steps) { - duration = Math.max(duration, startVibrating(step, nextSteps)); + for (SingleVibratorStep step : steps) { + long duration = startVibrating(step, nextSteps); + if (duration < 0) { + // One vibrator has failed, fail this entire sync attempt. + return maxDuration = -1; + } + maxDuration = Math.max(maxDuration, duration); } // Check if sync was prepared and if any step was accepted by a vibrator, // otherwise there is nothing to trigger here. - if (hasPrepared && duration > 0) { + if (hasPrepared && maxDuration > 0) { hasTriggered = mCallbacks.triggerSyncedVibration(mVibration.id); } - return duration; + return maxDuration; } finally { if (hasPrepared && !hasTriggered) { // Trigger has failed or all steps were ignored by the vibrators. mCallbacks.cancelSyncedVibration(); - return 0; + nextSteps.clear(); + } else if (maxDuration < 0) { + // Some vibrator failed without being prepared so other vibrators might be + // active. Cancel and remove every pending step from output list. + for (int i = nextSteps.size() - 1; i >= 0; i--) { + nextSteps.remove(i).cancel(); + } } } } } - private long startVibrating(VibratorOnStep step, List nextSteps) { + private long startVibrating(SingleVibratorStep step, List nextSteps) { nextSteps.addAll(step.play()); - return step.getDuration(); + long stepDuration = step.getOnResult(); + if (stepDuration < 0) { + // Step failed, so return negative duration to propagate failure. + return stepDuration; + } + // Return the longest estimation for the entire effect. + return Math.max(stepDuration, step.effect.getDuration()); } } @@ -592,121 +619,279 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { } /** - * Represent a step turn the vibrator on. - * - *

No other calls to the vibrator is made from this step, so this can be played in between - * calls to 'prepare' and 'trigger' for synchronized vibrations. + * Represent a step on a single vibrator that plays one or more segments from a + * {@link VibrationEffect.Composed} effect. */ - private final class VibratorOnStep extends Step { + private abstract class SingleVibratorStep extends Step { public final VibratorController controller; - public final VibrationEffect effect; - private long mDuration; + public final VibrationEffect.Composed effect; + public final int segmentIndex; + public final long vibratorOffTimeout; - VibratorOnStep(long startTime, VibratorController controller, VibrationEffect effect) { + long mVibratorOnResult; + + /** + * @param startTime The time to schedule this step in the {@link StepQueue}. + * @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 vibratorOffTimeout The time the vibrator is expected to complete any previous + * vibration and turn off. This is used to allow this step to be + * anticipated when the completion callback is triggered, and can + * be used play effects back-to-back. + */ + SingleVibratorStep(long startTime, VibratorController controller, + VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { super(startTime); this.controller = controller; this.effect = effect; + this.segmentIndex = index; + this.vibratorOffTimeout = vibratorOffTimeout; } /** - * Return the duration, in millis, of this effect. Repeating waveforms return {@link - * Long#MAX_VALUE}. Zero or negative values indicate the vibrator has ignored this effect. + * Return the result a call to {@link VibratorController#on} method triggered by + * {@link #play()}. + * + * @return A positive duration that the vibrator was turned on for by this step; + * Zero if the segment is not supported or vibrator was never turned on; + * A negative value if the vibrator call has failed. */ - public long getDuration() { - return mDuration; + public long getOnResult() { + return mVibratorOnResult; + } + + @Override + public boolean shouldPlayWhenVibratorComplete(int vibratorId) { + // Only anticipate 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 (controller.getVibratorInfo().getId() == vibratorId) + && (vibratorOffTimeout > SystemClock.uptimeMillis()); + } + + @Override + public void cancel() { + if (vibratorOffTimeout > SystemClock.uptimeMillis()) { + // Vibrator might be running from previous steps, so turn it off while canceling. + stopVibrating(); + } + } + + void stopVibrating() { + if (DEBUG) { + Slog.d(TAG, "Turning off vibrator " + controller.getVibratorInfo().getId()); + } + controller.off(); + } + + /** Return the {@link #nextVibrateStep} with same timings, only jumping the segments. */ + public List skipToNextSteps(int segmentsSkipped) { + return nextSteps(startTime, vibratorOffTimeout, segmentsSkipped); + } + + /** + * Return the {@link #nextVibrateStep} with same start and off timings calculated from + * {@link #getOnResult()}, jumping all played segments. + * + *

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. + */ + public List nextSteps(int segmentsPlayed) { + if (mVibratorOnResult <= 0) { + // Vibration was not started, so just skip the played segments and keep timings. + return skipToNextSteps(segmentsPlayed); + } + long nextVibratorOffTimeout = + SystemClock.uptimeMillis() + mVibratorOnResult + CALLBACKS_EXTRA_TIMEOUT; + return nextSteps(nextVibratorOffTimeout, nextVibratorOffTimeout, segmentsPlayed); + } + + /** + * Return the {@link #nextVibrateStep} with given start and off timings, which might be + * calculated independently, jumping all played segments. + * + *

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. + */ + public List nextSteps(long nextStartTime, long vibratorOffTimeout, + int segmentsPlayed) { + Step nextStep = nextVibrateStep(nextStartTime, controller, effect, + segmentIndex + segmentsPlayed, vibratorOffTimeout); + return nextStep == null ? EMPTY_STEP_LIST : Arrays.asList(nextStep); + } + } + + /** + * Represent a step turn the vibrator on with a single prebaked effect. + * + *

This step automatically falls back by replacing the prebaked segment with + * {@link VibrationSettings#getFallbackEffect(int)}, if available. + */ + private final class PerformStep extends SingleVibratorStep { + + PerformStep(long startTime, VibratorController controller, + VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { + super(startTime, controller, effect, index, vibratorOffTimeout); } @Override public List play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorOnStep"); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformStep"); try { - if (DEBUG) { - Slog.d(TAG, "Turning on vibrator " + controller.getVibratorInfo().getId()); + VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); + if (!(segment instanceof PrebakedSegment)) { + Slog.w(TAG, "Ignoring wrong segment for a PerformStep: " + segment); + return skipToNextSteps(/* segmentsSkipped= */ 1); } - List nextSteps = new ArrayList<>(); - mDuration = startVibrating(effect, nextSteps); - return nextSteps; + + PrebakedSegment prebaked = (PrebakedSegment) segment; + if (DEBUG) { + Slog.d(TAG, "Perform " + VibrationEffect.effectIdToString( + prebaked.getEffectId()) + " on vibrator " + + controller.getVibratorInfo().getId()); + } + + VibrationEffect fallback = mVibration.getFallback(prebaked.getEffectId()); + mVibratorOnResult = controller.on(prebaked, mVibration.id); + + if (mVibratorOnResult == 0 && prebaked.shouldFallback() + && (fallback instanceof VibrationEffect.Composed)) { + if (DEBUG) { + Slog.d(TAG, "Playing fallback for effect " + + VibrationEffect.effectIdToString(prebaked.getEffectId())); + } + SingleVibratorStep fallbackStep = nextVibrateStep(startTime, controller, + replaceCurrentSegment((VibrationEffect.Composed) fallback), + segmentIndex, vibratorOffTimeout); + List 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.getOnResult(); + return fallbackResult; + } + + return nextSteps(/* segmentsPlayed= */ 1); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } - private long startVibrating(VibrationEffect effect, List nextSteps) { - // TODO(b/167947076): split this into 4 different step implementations: - // VibratorPerformStep, VibratorComposePrimitiveStep, VibratorComposePwleStep and - // VibratorAmplitudeStep. - // Make sure each step carries over the full VibrationEffect and an incremental segment - // index, and triggers a final VibratorOffStep once all segments are done. - VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; - VibrationEffectSegment firstSegment = composed.getSegments().get(0); - final long duration; - final long now = SystemClock.uptimeMillis(); - if (firstSegment instanceof StepSegment) { - // Return the full duration of this waveform effect. - duration = effect.getDuration(); - long onDuration = getVibratorOnDuration(composed, 0); - if (onDuration > 0) { - // Do NOT set amplitude here. This might be called between prepareSynced and - // triggerSynced, so the vibrator is not actually turned on here. - // The next steps will handle the amplitudes after the vibrator has turned on. - controller.on(onDuration, mVibration.id); - } - long offTime = onDuration > 0 ? now + onDuration + CALLBACKS_EXTRA_TIMEOUT : now; - nextSteps.add(new VibratorAmplitudeStep(now, controller, composed, offTime)); - } else if (firstSegment instanceof PrebakedSegment) { - PrebakedSegment prebaked = (PrebakedSegment) firstSegment; - VibrationEffect fallback = mVibration.getFallback(prebaked.getEffectId()); - duration = controller.on(prebaked, mVibration.id); - if (duration > 0) { - nextSteps.add(new VibratorOffStep(now + duration + CALLBACKS_EXTRA_TIMEOUT, - controller)); - } else if (prebaked.shouldFallback() && fallback != null) { - return startVibrating(fallback, nextSteps); - } - } else if (firstSegment instanceof PrimitiveSegment) { - int segmentCount = composed.getSegments().size(); - PrimitiveSegment[] primitives = new PrimitiveSegment[segmentCount]; - for (int i = 0; i < segmentCount; i++) { - VibrationEffectSegment segment = composed.getSegments().get(i); + /** + * Replace segment at {@link #segmentIndex} in {@link #effect} with given fallback segments. + * + * @return a copy of {@link #effect} with replaced segment. + */ + private VibrationEffect.Composed replaceCurrentSegment(VibrationEffect.Composed fallback) { + List newSegments = new ArrayList<>(effect.getSegments()); + int newRepeatIndex = effect.getRepeatIndex(); + newSegments.remove(segmentIndex); + newSegments.addAll(segmentIndex, fallback.getSegments()); + if (segmentIndex < effect.getRepeatIndex()) { + newRepeatIndex += fallback.getSegments().size(); + } + return new VibrationEffect.Composed(newSegments, newRepeatIndex); + } + } + + /** + * Represent a step turn the vibrator on using a composition of primitives. + * + *

This step will use the maximum supported number of consecutive segments of type + * {@link PrimitiveSegment} starting at the current index. + */ + private final class ComposePrimitivesStep extends SingleVibratorStep { + + ComposePrimitivesStep(long startTime, VibratorController controller, + VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { + super(startTime, controller, effect, index, vibratorOffTimeout); + } + + @Override + public List play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep"); + try { + int segmentCount = effect.getSegments().size(); + List primitives = new ArrayList<>(); + for (int i = segmentIndex; i < segmentCount; i++) { + VibrationEffectSegment segment = effect.getSegments().get(i); if (segment instanceof PrimitiveSegment) { - primitives[i] = (PrimitiveSegment) segment; + primitives.add((PrimitiveSegment) segment); } else { - primitives[i] = new PrimitiveSegment( - VibrationEffect.Composition.PRIMITIVE_NOOP, - /* scale= */ 1, /* delay= */ 0); + break; } } - duration = controller.on(primitives, mVibration.id); - if (duration > 0) { - nextSteps.add(new VibratorOffStep(now + duration + CALLBACKS_EXTRA_TIMEOUT, - controller)); + + if (primitives.isEmpty()) { + Slog.w(TAG, "Ignoring wrong segment for a ComposePrimitivesStep: " + + effect.getSegments().get(segmentIndex)); + return skipToNextSteps(/* segmentsSkipped= */ 1); } - } else if (firstSegment instanceof RampSegment) { - int segmentCount = composed.getSegments().size(); - RampSegment[] primitives = new RampSegment[segmentCount]; - for (int i = 0; i < segmentCount; i++) { - VibrationEffectSegment segment = composed.getSegments().get(i); + + if (DEBUG) { + Slog.d(TAG, "Compose " + primitives.size() + " primitives on vibrator " + + controller.getVibratorInfo().getId()); + } + mVibratorOnResult = controller.on( + primitives.toArray(new PrimitiveSegment[primitives.size()]), + mVibration.id); + + return nextSteps(/* segmntsPlayed= */ primitives.size()); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + } + + /** + * Represent a step turn the vibrator on using a composition of PWLE segments. + * + *

This step will use the maximum supported number of consecutive segments of type + * {@link StepSegment} or {@link RampSegment} starting at the current index. + */ + private final class ComposePwleStep extends SingleVibratorStep { + + ComposePwleStep(long startTime, VibratorController controller, + VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { + super(startTime, controller, effect, index, vibratorOffTimeout); + } + + @Override + public List play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep"); + try { + int segmentCount = effect.getSegments().size(); + List pwles = new ArrayList<>(); + for (int i = segmentIndex; i < segmentCount; i++) { + VibrationEffectSegment segment = effect.getSegments().get(i); if (segment instanceof RampSegment) { - primitives[i] = (RampSegment) segment; + pwles.add((RampSegment) segment); } else if (segment instanceof StepSegment) { StepSegment stepSegment = (StepSegment) segment; - primitives[i] = new RampSegment( - stepSegment.getAmplitude(), stepSegment.getAmplitude(), - stepSegment.getFrequency(), stepSegment.getFrequency(), - (int) stepSegment.getDuration()); + pwles.add(new RampSegment(stepSegment.getAmplitude(), + stepSegment.getAmplitude(), stepSegment.getFrequency(), + stepSegment.getFrequency(), (int) stepSegment.getDuration())); } else { - primitives[i] = new RampSegment(0, 0, 0, 0, 0); + break; } } - duration = controller.on(primitives, mVibration.id); - if (duration > 0) { - nextSteps.add(new VibratorOffStep(now + duration + CALLBACKS_EXTRA_TIMEOUT, - controller)); + + if (pwles.isEmpty()) { + Slog.w(TAG, "Ignoring wrong segment for a ComposePwleStep: " + + effect.getSegments().get(segmentIndex)); + return skipToNextSteps(/* segmentsSkipped= */ 1); } - } else { - duration = 0; + + if (DEBUG) { + Slog.d(TAG, "Compose " + pwles.size() + " PWLEs on vibrator " + + controller.getVibratorInfo().getId()); + } + mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]), + mVibration.id); + + return nextSteps(/* segmentsPlayed= */ pwles.size()); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } - return duration; } } @@ -716,22 +901,15 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { *

This runs after a timeout on the expected time the vibrator should have finished playing, * and can anticipated by vibrator complete callbacks. */ - private final class VibratorOffStep extends Step { - public final VibratorController controller; + private final class OffStep extends SingleVibratorStep { - VibratorOffStep(long startTime, VibratorController controller) { - super(startTime); - this.controller = controller; - } - - @Override - public boolean shouldPlayWhenVibratorComplete(int vibratorId) { - return controller.getVibratorInfo().getId() == vibratorId; + OffStep(long startTime, VibratorController controller) { + super(startTime, controller, /* effect= */ null, /* index= */ -1, startTime); } @Override public List play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorOffStep"); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "OffStep"); try { stopVibrating(); return EMPTY_STEP_LIST; @@ -739,108 +917,88 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } - - @Override - public void cancel() { - stopVibrating(); - } - - private void stopVibrating() { - if (DEBUG) { - Slog.d(TAG, "Turning off vibrator " + controller.getVibratorInfo().getId()); - } - controller.off(); - } } - /** Represents a step to change the amplitude of the vibrator. */ - private final class VibratorAmplitudeStep extends Step { - public final VibratorController controller; - public final VibrationEffect.Composed effect; - public final int currentIndex; + /** + * Represents a step to turn the vibrator on and change its amplitude. + * + *

This step ignores vibration completion callbacks and control the vibrator on/off state + * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}. + */ + private final class AmplitudeStep extends SingleVibratorStep { + private long mNextOffTime; - private long mNextVibratorStopTime; - - VibratorAmplitudeStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, long expectedVibratorStopTime) { - this(startTime, controller, effect, /* index= */ 0, expectedVibratorStopTime); - } - - VibratorAmplitudeStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, int index, long expectedVibratorStopTime) { - super(startTime); - this.controller = controller; - this.effect = effect; - this.currentIndex = index; - mNextVibratorStopTime = expectedVibratorStopTime; + AmplitudeStep(long startTime, VibratorController controller, + VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { + super(startTime, controller, effect, index, vibratorOffTimeout); + mNextOffTime = vibratorOffTimeout; } @Override public boolean shouldPlayWhenVibratorComplete(int vibratorId) { if (controller.getVibratorInfo().getId() == vibratorId) { - mNextVibratorStopTime = SystemClock.uptimeMillis(); + mNextOffTime = SystemClock.uptimeMillis(); } + // Timings are tightly controlled here, so never anticipate when vibrator is complete. return false; } @Override public List play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorAmplitudeStep"); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "AmplitudeStep"); try { if (DEBUG) { long latency = SystemClock.uptimeMillis() - startTime; Slog.d(TAG, "Running amplitude step with " + latency + "ms latency."); } - VibrationEffectSegment segment = effect.getSegments().get(currentIndex); + + VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); if (!(segment instanceof StepSegment)) { - return nextSteps(); + Slog.w(TAG, "Ignoring wrong segment for a AmplitudeStep: " + segment); + return skipToNextSteps(/* segmentsSkipped= */ 1); } + StepSegment stepSegment = (StepSegment) segment; if (stepSegment.getDuration() == 0) { // Skip waveform entries with zero timing. - return nextSteps(); + return skipToNextSteps(/* segmentsSkipped= */ 1); } + + long now = SystemClock.uptimeMillis(); float amplitude = stepSegment.getAmplitude(); if (amplitude == 0) { - stopVibrating(); - return nextSteps(); - } - if (startTime >= mNextVibratorStopTime) { - // Vibrator has stopped. Turn vibrator back on for the duration of another - // cycle before setting the amplitude. - long onDuration = getVibratorOnDuration(effect, currentIndex); - if (onDuration > 0) { - startVibrating(onDuration); - mNextVibratorStopTime = - SystemClock.uptimeMillis() + onDuration + CALLBACKS_EXTRA_TIMEOUT; + if (mNextOffTime > now) { + // Amplitude cannot be set to zero, so stop the vibrator. + stopVibrating(); + mNextOffTime = now; } + } else { + if (startTime >= mNextOffTime) { + // Vibrator has stopped. 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 + CALLBACKS_EXTRA_TIMEOUT; + } + } + changeAmplitude(amplitude); } - changeAmplitude(amplitude); - return nextSteps(); + + // Use original startTime to avoid propagating latencies to the waveform. + long nextStartTime = startTime + segment.getDuration(); + return nextSteps(nextStartTime, mNextOffTime, /* segmentsPlayed= */ 1); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } - @Override - public void cancel() { - stopVibrating(); - } - - private void stopVibrating() { - if (DEBUG) { - Slog.d(TAG, "Turning off vibrator " + controller.getVibratorInfo().getId()); - } - controller.off(); - mNextVibratorStopTime = SystemClock.uptimeMillis(); - } - - private void startVibrating(long duration) { + private long startVibrating(long duration) { if (DEBUG) { Slog.d(TAG, "Turning on vibrator " + controller.getVibratorInfo().getId() + " for " + duration + "ms"); } - controller.on(duration, mVibration.id); + return controller.on(duration, mVibration.id); } private void changeAmplitude(float amplitude) { @@ -851,18 +1009,35 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { controller.setAmplitude(amplitude); } - @NonNull - private List nextSteps() { - long nextStartTime = startTime + effect.getSegments().get(currentIndex).getDuration(); - int nextIndex = currentIndex + 1; - if (nextIndex >= effect.getSegments().size()) { - nextIndex = effect.getRepeatIndex(); + /** + * Get the duration the vibrator will be on for a waveform, starting at {@code startIndex} + * until the next time it's vibrating amplitude is zero or a different type of segment is + * found. + */ + private long getVibratorOnDuration(VibrationEffect.Composed effect, int startIndex) { + List segments = effect.getSegments(); + int segmentCount = segments.size(); + int repeatIndex = effect.getRepeatIndex(); + int i = startIndex; + long timing = 0; + while (i < segmentCount) { + VibrationEffectSegment segment = segments.get(i); + if (!(segment instanceof StepSegment) + || ((StepSegment) segment).getAmplitude() == 0) { + break; + } + timing += segment.getDuration(); + i++; + if (i == segmentCount && repeatIndex >= 0) { + i = repeatIndex; + // prevent infinite loop + repeatIndex = -1; + } + if (i == startIndex) { + return 1000; + } } - Step nextStep = nextIndex < 0 - ? new VibratorOffStep(nextStartTime, controller) - : new VibratorAmplitudeStep(nextStartTime, controller, effect, nextIndex, - mNextVibratorStopTime); - return Arrays.asList(nextStep); + return timing; } } @@ -873,7 +1048,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { * play all of the effects in sync. */ private final class DeviceEffectMap { - private final SparseArray mVibratorEffects; + private final SparseArray mVibratorEffects; private final int[] mVibratorIds; private final long mRequiredSyncCapabilities; @@ -884,8 +1059,10 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { int vibratorId = mVibrators.keyAt(i); VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo(); VibrationEffect effect = mDeviceEffectAdapter.apply(mono.getEffect(), vibratorInfo); - mVibratorEffects.put(vibratorId, effect); - mVibratorIds[i] = vibratorId; + if (effect instanceof VibrationEffect.Composed) { + mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect); + mVibratorIds[i] = vibratorId; + } } mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects); } @@ -899,7 +1076,9 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo(); VibrationEffect effect = mDeviceEffectAdapter.apply( stereoEffects.valueAt(i), vibratorInfo); - mVibratorEffects.put(vibratorId, effect); + if (effect instanceof VibrationEffect.Composed) { + mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect); + } } } mVibratorIds = new int[mVibratorEffects.size()]; @@ -937,7 +1116,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { } /** Return the {@link VibrationEffect} at given index. */ - public VibrationEffect effectAt(int index) { + public VibrationEffect.Composed effectAt(int index) { return mVibratorEffects.valueAt(index); } @@ -948,11 +1127,11 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { * @return {@link IVibratorManager#CAP_SYNC} together with all required * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities. */ - private long calculateRequiredSyncCapabilities(SparseArray effects) { + private long calculateRequiredSyncCapabilities( + SparseArray effects) { long prepareCap = 0; for (int i = 0; i < effects.size(); i++) { - VibrationEffect.Composed composed = (VibrationEffect.Composed) effects.valueAt(i); - VibrationEffectSegment firstSegment = composed.getSegments().get(0); + VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0); if (firstSegment instanceof StepSegment) { prepareCap |= IVibratorManager.CAP_PREPARE_ON; } else if (firstSegment instanceof PrebakedSegment) { diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index c8977025b846e..dfdd86358347f 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -189,11 +189,17 @@ final class VibratorController { * callback to {@link OnVibrationCompleteListener}. * *

This will affect the state of {@link #isVibrating()}. + * + * @return The positive duration of the vibration started, if successful, zero if the vibrator + * do not support the input or a negative number if the operation failed. */ - public void on(long milliseconds, long vibrationId) { + public long on(long milliseconds, long vibrationId) { synchronized (mLock) { - mNativeWrapper.on(milliseconds, vibrationId); - notifyVibratorOnLocked(); + long duration = mNativeWrapper.on(milliseconds, vibrationId); + if (duration > 0) { + notifyVibratorOnLocked(); + } + return duration; } } @@ -203,7 +209,8 @@ final class VibratorController { * *

This will affect the state of {@link #isVibrating()}. * - * @return The duration of the effect playing, or 0 if unsupported. + * @return The positive duration of the vibration started, if successful, zero if the vibrator + * do not support the input or a negative number if the operation failed. */ public long on(PrebakedSegment prebaked, long vibrationId) { synchronized (mLock) { @@ -222,7 +229,8 @@ final class VibratorController { * *

This will affect the state of {@link #isVibrating()}. * - * @return The duration of the effect playing, or 0 if unsupported. + * @return The positive duration of the vibration started, if successful, zero if the vibrator + * do not support the input or a negative number if the operation failed. */ public long on(PrimitiveSegment[] primitives, long vibrationId) { if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { @@ -327,7 +335,7 @@ final class VibratorController { */ private static native long getNativeFinalizer(); private static native boolean isAvailable(long nativePtr); - private static native void on(long nativePtr, long milliseconds, long vibrationId); + private static native long on(long nativePtr, long milliseconds, long vibrationId); private static native void off(long nativePtr); private static native void setAmplitude(long nativePtr, float amplitude); private static native int[] getSupportedEffects(long nativePtr); @@ -365,8 +373,8 @@ final class VibratorController { } /** Turns vibrator on for given time. */ - public void on(long milliseconds, long vibrationId) { - on(mNativePtr, milliseconds, vibrationId); + public long on(long milliseconds, long vibrationId) { + return on(mNativePtr, milliseconds, vibrationId); } /** Turns vibrator off. */ diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 5fd1d7a6e1dc8..f28a559478829 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -1394,15 +1394,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { while ((nextArg = peekNextArg()) != null) { switch (nextArg) { case "-f": - getNextArgRequired(); // consume the -f argument; + getNextArgRequired(); // consume "-f" force = true; break; case "-d": - getNextArgRequired(); // consume the -d argument; + getNextArgRequired(); // consume "-d" description = getNextArgRequired(); break; default: - // Not a common option, finish reading. + // nextArg is not a common option, finish reading. return; } } @@ -1456,14 +1456,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private int runMono() { CommonOptions commonOptions = new CommonOptions(); - VibrationEffect effect = nextEffect(); - if (effect == null) { - return 0; - } - - CombinedVibrationEffect combinedEffect = CombinedVibrationEffect.createSynced(effect); + CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(nextEffect()); VibrationAttributes attrs = createVibrationAttributes(commonOptions); - vibrate(Binder.getCallingUid(), SHELL_PACKAGE_NAME, combinedEffect, attrs, + vibrate(Binder.getCallingUid(), SHELL_PACKAGE_NAME, effect, attrs, commonOptions.description, mToken); return 0; } @@ -1474,10 +1469,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { CombinedVibrationEffect.startSynced(); while ("-v".equals(getNextOption())) { int vibratorId = Integer.parseInt(getNextArgRequired()); - VibrationEffect effect = nextEffect(); - if (effect != null) { - combination.addVibrator(vibratorId, effect); - } + combination.addVibrator(vibratorId, nextEffect()); } VibrationAttributes attrs = createVibrationAttributes(commonOptions); vibrate(Binder.getCallingUid(), SHELL_PACKAGE_NAME, combination.combine(), attrs, @@ -1487,19 +1479,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private int runSequential() { CommonOptions commonOptions = new CommonOptions(); - CombinedVibrationEffect.SequentialCombination combination = CombinedVibrationEffect.startSequential(); while ("-v".equals(getNextOption())) { int vibratorId = Integer.parseInt(getNextArgRequired()); - int delay = 0; - if ("-w".equals(getNextOption())) { - delay = Integer.parseInt(getNextArgRequired()); - } - VibrationEffect effect = nextEffect(); - if (effect != null) { - combination.addNext(vibratorId, effect, delay); - } + combination.addNext(vibratorId, nextEffect()); } VibrationAttributes attrs = createVibrationAttributes(commonOptions); vibrate(Binder.getCallingUid(), SHELL_PACKAGE_NAME, combination.combine(), attrs, @@ -1512,87 +1496,129 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return 0; } - @Nullable private VibrationEffect nextEffect() { - String effectType = getNextArgRequired(); - if ("oneshot".equals(effectType)) { - return nextOneShot(); + VibrationEffect.Composition composition = VibrationEffect.startComposition(); + String nextArg; + + while ((nextArg = peekNextArg()) != null) { + if ("oneshot".equals(nextArg)) { + addOneShotToComposition(composition); + } else if ("waveform".equals(nextArg)) { + addWaveformToComposition(composition); + } else if ("prebaked".equals(nextArg)) { + addPrebakedToComposition(composition); + } else if ("primitives".equals(nextArg)) { + addPrimitivesToComposition(composition); + } else { + // nextArg is not an effect, finish reading. + break; + } } - if ("waveform".equals(effectType)) { - return nextWaveform(); - } - if ("prebaked".equals(effectType)) { - return nextPrebaked(); - } - if ("composed".equals(effectType)) { - return nextComposed(); - } - return null; + + return composition.compose(); } - private VibrationEffect nextOneShot() { - boolean hasAmplitude = "-a".equals(getNextOption()); + private void addOneShotToComposition(VibrationEffect.Composition composition) { + boolean hasAmplitude = false; + int delay = 0; + + getNextArgRequired(); // consume "oneshot" + String nextOption; + while ((nextOption = getNextOption()) != null) { + if ("-a".equals(nextOption)) { + hasAmplitude = true; + } else if ("-w".equals(nextOption)) { + delay = Integer.parseInt(getNextArgRequired()); + } + } + long duration = Long.parseLong(getNextArgRequired()); int amplitude = hasAmplitude ? Integer.parseInt(getNextArgRequired()) : VibrationEffect.DEFAULT_AMPLITUDE; - return VibrationEffect.createOneShot(duration, amplitude); + composition.addEffect(VibrationEffect.createOneShot(duration, amplitude), delay); } - private VibrationEffect nextWaveform() { + private void addWaveformToComposition(VibrationEffect.Composition composition) { boolean hasAmplitudes = false; int repeat = -1; + int delay = 0; - String nextOption = getNextOption(); - while (nextOption != null) { + getNextArgRequired(); // consume "waveform" + String nextOption; + while ((nextOption = getNextOption()) != null) { if ("-a".equals(nextOption)) { hasAmplitudes = true; } else if ("-r".equals(nextOption)) { repeat = Integer.parseInt(getNextArgRequired()); + } else if ("-w".equals(nextOption)) { + delay = Integer.parseInt(getNextArgRequired()); } - nextOption = getNextOption(); } List durations = new ArrayList<>(); List amplitudes = new ArrayList<>(); + VibrationEffect waveform; String nextArg; - while ((nextArg = peekNextArg()) != null && !"-v".equals(nextArg)) { - durations.add(Long.parseLong(getNextArgRequired())); + while ((nextArg = peekNextArg()) != null) { + try { + durations.add(Long.parseLong(nextArg)); + getNextArgRequired(); // consume the duration + } catch (NumberFormatException e) { + // nextArg is not a duration, finish reading. + break; + } if (hasAmplitudes) { amplitudes.add(Integer.parseInt(getNextArgRequired())); } } long[] durationArray = durations.stream().mapToLong(Long::longValue).toArray(); - if (!hasAmplitudes) { - return VibrationEffect.createWaveform(durationArray, repeat); + if (hasAmplitudes) { + int[] amplitudeArray = amplitudes.stream().mapToInt(Integer::intValue).toArray(); + waveform = VibrationEffect.createWaveform(durationArray, amplitudeArray, repeat); + } else { + waveform = VibrationEffect.createWaveform(durationArray, repeat); } - int[] amplitudeArray = amplitudes.stream().mapToInt(Integer::intValue).toArray(); - return VibrationEffect.createWaveform(durationArray, amplitudeArray, repeat); + composition.addEffect(waveform, delay); } - private VibrationEffect nextPrebaked() { - boolean shouldFallback = "-b".equals(getNextOption()); + private void addPrebakedToComposition(VibrationEffect.Composition composition) { + boolean shouldFallback = false; + int delay = 0; + + getNextArgRequired(); // consume "prebaked" + String nextOption; + while ((nextOption = getNextOption()) != null) { + if ("-b".equals(nextOption)) { + shouldFallback = true; + } else if ("-w".equals(nextOption)) { + delay = Integer.parseInt(getNextArgRequired()); + } + } + int effectId = Integer.parseInt(getNextArgRequired()); - return VibrationEffect.get(effectId, shouldFallback); + composition.addEffect(VibrationEffect.get(effectId, shouldFallback), delay); } - private VibrationEffect nextComposed() { - VibrationEffect.Composition composition = VibrationEffect.startComposition(); + private void addPrimitivesToComposition(VibrationEffect.Composition composition) { + getNextArgRequired(); // consume "primitives" String nextArg; while ((nextArg = peekNextArg()) != null) { int delay = 0; if ("-w".equals(nextArg)) { - getNextArgRequired(); // consume the -w option + getNextArgRequired(); // consume "-w" delay = Integer.parseInt(getNextArgRequired()); - } else if ("-v".equals(nextArg)) { - // Starting next vibrator, this composed effect if finished. + nextArg = peekNextArg(); + } + try { + composition.addPrimitive(Integer.parseInt(nextArg), /* scale= */ 1, delay); + getNextArgRequired(); // consume the primitive id + } catch (NumberFormatException | NullPointerException e) { + // nextArg is not describing a primitive, leave it to be consumed by outer loops break; } - int primitiveId = Integer.parseInt(getNextArgRequired()); - composition.addPrimitive(primitiveId, /* scale= */ 1f, delay); } - return composition.compose(); } private VibrationAttributes createVibrationAttributes(CommonOptions commonOptions) { @@ -1615,38 +1641,44 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" list"); pw.println(" Prints the id of device vibrators. This does not include any "); pw.println(" connected input device."); - pw.println(" synced [options] "); + pw.println(" synced [options] ..."); pw.println(" Vibrates effect on all vibrators in sync."); - pw.println(" combined [options] (-v )..."); + pw.println(" combined [options] (-v ...)..."); pw.println(" Vibrates different effects on each vibrator in sync."); - pw.println(" sequential [options] (-v [-w ] )..."); + pw.println(" sequential [options] (-v ...)..."); pw.println(" Vibrates different effects on each vibrator in sequence."); pw.println(" cancel"); pw.println(" Cancels any active vibration"); pw.println(""); pw.println("Effect commands:"); - pw.println(" oneshot [-a] []"); + pw.println(" oneshot [-w delay] [-a] []"); pw.println(" Vibrates for duration milliseconds; ignored when device is on "); pw.println(" DND (Do Not Disturb) mode; touch feedback strength user setting "); pw.println(" will be used to scale amplitude."); + pw.println(" If -w is provided, the effect will be played after the specified"); + pw.println(" wait time in milliseconds."); pw.println(" If -a is provided, the command accepts a second argument for "); pw.println(" amplitude, in a scale of 1-255."); - pw.println(" waveform [-r ] [-a] ( [])..."); + pw.println(" waveform [-w delay] [-r index] [-a] ( [])..."); pw.println(" Vibrates for durations and amplitudes in list; ignored when "); pw.println(" device is on DND (Do Not Disturb) mode; touch feedback strength "); pw.println(" user setting will be used to scale amplitude."); + pw.println(" If -w is provided, the effect will be played after the specified"); + pw.println(" wait time in milliseconds."); pw.println(" If -r is provided, the waveform loops back to the specified"); pw.println(" index (e.g. 0 loops from the beginning)"); pw.println(" If -a is provided, the command accepts duration-amplitude pairs;"); pw.println(" otherwise, it accepts durations only and alternates off/on"); pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255."); - pw.println(" prebaked [-b] "); + pw.println(" prebaked [-w delay] [-b] "); pw.println(" Vibrates with prebaked effect; ignored when device is on DND "); pw.println(" (Do Not Disturb) mode; touch feedback strength user setting "); pw.println(" will be used to scale amplitude."); + pw.println(" If -w is provided, the effect will be played after the specified"); + pw.println(" wait time in milliseconds."); pw.println(" If -b is provided, the prebaked fallback effect will be played if"); pw.println(" the device doesn't support the given effect-id."); - pw.println(" composed [-w ] ..."); + pw.println(" primitives ([-w delay] )..."); pw.println(" Vibrates with a composed effect; ignored when device is on DND "); pw.println(" (Do Not Disturb) mode; touch feedback strength user setting "); pw.println(" will be used to scale primitive intensities."); diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index 7f8168af944aa..caec703a4c673 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -150,15 +150,16 @@ static jboolean vibratorIsAvailable(JNIEnv* env, jclass /* clazz */, jlong ptr) return wrapper->hal()->ping().isOk() ? JNI_TRUE : JNI_FALSE; } -static void vibratorOn(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong timeoutMs, - jlong vibrationId) { +static jlong vibratorOn(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong timeoutMs, + jlong vibrationId) { VibratorControllerWrapper* wrapper = reinterpret_cast(ptr); if (wrapper == nullptr) { ALOGE("vibratorOn failed because native wrapper was not initialized"); - return; + return -1; } auto callback = wrapper->createCallback(vibrationId); - wrapper->hal()->on(std::chrono::milliseconds(timeoutMs), callback); + auto result = wrapper->hal()->on(std::chrono::milliseconds(timeoutMs), callback); + return result.isOk() ? timeoutMs : (result.isUnsupported() ? 0 : -1); } static void vibratorOff(JNIEnv* env, jclass /* clazz */, jlong ptr) { @@ -234,7 +235,7 @@ static jlong vibratorPerformEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, j aidl::EffectStrength effectStrength = static_cast(strength); auto callback = wrapper->createCallback(vibrationId); auto result = wrapper->hal()->performEffect(effectType, effectStrength, callback); - return result.isOk() ? result.value().count() : -1; + return result.isOk() ? result.value().count() : (result.isUnsupported() ? 0 : -1); } static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, @@ -252,7 +253,7 @@ static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlon } auto callback = wrapper->createCallback(vibrationId); auto result = wrapper->hal()->performComposedEffect(effects, callback); - return result.isOk() ? result.value().count() : -1; + return result.isOk() ? result.value().count() : (result.isUnsupported() ? 0 : -1); } static jlong vibratorGetCapabilities(JNIEnv* env, jclass /* clazz */, jlong ptr) { @@ -311,7 +312,7 @@ static const JNINativeMethod method_table[] = { (void*)vibratorNativeInit}, {"getNativeFinalizer", "()J", (void*)vibratorGetNativeFinalizer}, {"isAvailable", "(J)Z", (void*)vibratorIsAvailable}, - {"on", "(JJJ)V", (void*)vibratorOn}, + {"on", "(JJJ)J", (void*)vibratorOn}, {"off", "(J)V", (void*)vibratorOff}, {"setAmplitude", "(JF)V", (void*)vibratorSetAmplitude}, {"performEffect", "(JJJJ)J", (void*)vibratorPerformEffect}, diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java index 1e3c344748209..f7e7688a71f4e 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -74,11 +74,12 @@ final class FakeVibratorControllerProvider { } @Override - public void on(long milliseconds, long vibrationId) { + public long on(long milliseconds, long vibrationId) { mEffectSegments.add(new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequency= */ 0, (int) milliseconds)); applyLatency(); scheduleListener(milliseconds, vibrationId); + return milliseconds; } @Override 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 37e0ec2159d92..00ac55d7ad236 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -381,6 +382,42 @@ public class VibrationThreadTest { assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments().isEmpty()); } + @Test + public void vibrate_singleVibratorComposedEffects_runsDifferentVibrations() throws Exception { + mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK, + VibrationEffect.Composition.PRIMITIVE_TICK); + mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS, + IVibrator.CAP_AMPLITUDE_CONTROL); + + long vibrationId = 1; + VibrationEffect effect = VibrationEffect.startComposition() + .addEffect(VibrationEffect.createOneShot(10, 100)) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) + .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK), 10) + .compose(); + VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); + waitForCompletion(thread); + + // Use first duration the vibrator is turned on since we cannot estimate the clicks. + verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L)); + verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); + verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED)); + assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); + assertEquals(Arrays.asList( + expectedOneShot(10), + expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0), + expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0), + expectedPrebaked(VibrationEffect.EFFECT_CLICK), + expectedPrebaked(VibrationEffect.EFFECT_CLICK)), + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments()); + assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()); + } + @Test public void vibrate_singleVibratorCancelled_vibratorStopped() throws Exception { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java index 70ea219911b7e..52a9783121d5c 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java @@ -191,6 +191,7 @@ public class VibratorControllerTest { @Test public void on_withDuration_turnsVibratorOn() { + when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); controller.on(100, 10); @@ -241,7 +242,9 @@ public class VibratorControllerTest { @Test public void off_turnsOffVibrator() { + when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); + controller.on(100, 1); assertTrue(controller.isVibrating()); @@ -253,6 +256,7 @@ public class VibratorControllerTest { @Test public void registerVibratorStateListener_callbacksAreTriggered() throws Exception { + when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); controller.registerVibratorStateListener(mVibratorStateListenerMock); @@ -271,6 +275,7 @@ public class VibratorControllerTest { @Test public void unregisterVibratorStateListener_callbackNotTriggeredAfter() throws Exception { + when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); controller.registerVibratorStateListener(mVibratorStateListenerMock);