Play any composition in VibrationThread

Remove VibratorOnStep and create a individual step for each IVibrator
method that can be used to turn on the vibrator (on, perform, compose or
composePwle).

Playing compositions now is implemented in the same was as playing a
waveform with setAmplitude calls is implemented: a sequence of steps.

Bug: 167947076
Test: VibrationThreadTest
Change-Id: I0c0c031770b1a01ab5a74b1af15b2c3904c2d6a4
This commit is contained in:
Lais Andrade
2021-03-17 17:52:17 +00:00
parent a411746184
commit 1bd6aac400
7 changed files with 562 additions and 299 deletions

View File

@@ -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<VibrationEffectSegment> 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<Step> nextSteps) {
private long startVibrating(SingleVibratorStep step, List<Step> 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.
*
* <p>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<Step> 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.
*
* <p>This method has same behavior as {@link #skipToNextSteps(int)} when the vibrator
* result is non-positive, meaning the vibrator has either ignored or failed to turn on.
*/
public List<Step> 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.
*
* <p>This should be used when the vibrator on/off state is not responsible for the steps
* execution timings, e.g. while playing the vibrator amplitudes.
*/
public List<Step> 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.
*
* <p>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<Step> 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<Step> 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<Step> fallbackResult = fallbackStep.play();
// Update the result with the fallback result so this step is seamlessly
// replaced by the fallback to any outer application of this.
mVibratorOnResult = fallbackStep.getOnResult();
return fallbackResult;
}
return nextSteps(/* segmentsPlayed= */ 1);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
private long startVibrating(VibrationEffect effect, List<Step> 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<VibrationEffectSegment> 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.
*
* <p>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<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep");
try {
int segmentCount = 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[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.
*
* <p>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<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep");
try {
int segmentCount = 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) {
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 {
* <p>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<Step> 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.
*
* <p>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<Step> 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<Step> 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<VibrationEffectSegment> 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<VibrationEffect> mVibratorEffects;
private final SparseArray<VibrationEffect.Composed> 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<VibrationEffect> effects) {
private long calculateRequiredSyncCapabilities(
SparseArray<VibrationEffect.Composed> 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) {

View File

@@ -189,11 +189,17 @@ final class VibratorController {
* callback to {@link OnVibrationCompleteListener}.
*
* <p>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 {
*
* <p>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 {
*
* <p>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. */

View File

@@ -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<Long> durations = new ArrayList<>();
List<Integer> 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] <effect>");
pw.println(" synced [options] <effect>...");
pw.println(" Vibrates effect on all vibrators in sync.");
pw.println(" combined [options] (-v <vibrator-id> <effect>)...");
pw.println(" combined [options] (-v <vibrator-id> <effect>...)...");
pw.println(" Vibrates different effects on each vibrator in sync.");
pw.println(" sequential [options] (-v <vibrator-id> [-w <delay>] <effect>)...");
pw.println(" sequential [options] (-v <vibrator-id> <effect>...)...");
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] <duration> [<amplitude>]");
pw.println(" oneshot [-w delay] [-a] <duration> [<amplitude>]");
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 <index>] [-a] (<duration> [<amplitude>])...");
pw.println(" waveform [-w delay] [-r index] [-a] (<duration> [<amplitude>])...");
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] <effect-id>");
pw.println(" prebaked [-w delay] [-b] <effect-id>");
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 <delay>] <primitive-id>...");
pw.println(" primitives ([-w delay] <primitive-id>)...");
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.");

View File

@@ -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<VibratorControllerWrapper*>(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<aidl::EffectStrength>(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},

View File

@@ -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

View File

@@ -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);

View File

@@ -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);