Extract Steps and StepQueue out of VibrationThread.

This is groundwork for making VibrationThread long-lived, executing
vibrations managed by a VibrationStepConductor(StepQueue) in turn. The
purpose is to separate state from the two, but I thought we might as
well pull the steps right out to the top-level rather than moving them
to be nested in the conductor, as it's easier to navigate conductor code
then.

Renames:
 - StepQueue to VibrationStepConductor
 - SingleVibratorStep to AbstractVibratorStep
 - all subclasses of SingleVibratorStep to be called
   SomethingVibratorStep.
 - The non-VibratorStep Steps are now SomethingSequentialEffectStep.

This CL is intended to have no logic changes - just path-to-access, but
needs to move fields and methods around a lot, and change visibility to
make it work. In some cases, the field visibility arrangement is not
great (especially the lock used in VibrationThread), but the worst of
those will change in a follow-up that will detach VibrationStepConductor
from VibrationThread that will require non-trivial changes.

One small refactor for neatness was to move "noteVibratorOn/Off" into
the VibratorManagerHooks so that the battery stats service itself isn't
needed.

Bug: 193792066
Test: atest
Change-Id: Iabed5a057b0e0f65450c9ed9ef01186055f21907
This commit is contained in:
Simon Bowden
2022-02-16 17:54:42 +00:00
parent 45a0712218
commit 7ca22de115
16 changed files with 1915 additions and 1460 deletions

View File

@@ -0,0 +1,148 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.util.Slog;
import java.util.Arrays;
import java.util.List;
/**
* Represent a step on a single vibrator that plays one or more segments from a
* {@link VibrationEffect.Composed} effect.
*/
abstract class AbstractVibratorStep extends Step {
public final VibratorController controller;
public final VibrationEffect.Composed effect;
public final int segmentIndex;
public final long previousStepVibratorOffTimeout;
long mVibratorOnResult;
boolean mVibratorCompleteCallbackReceived;
/**
* @param conductor The VibrationStepConductor for these steps.
* @param startTime The time to schedule this step in the
* {@link VibrationStepConductor}.
* @param controller The vibrator that is playing the effect.
* @param effect The effect being played in this step.
* @param index The index of the next segment to be played by this step
* @param previousStepVibratorOffTimeout The time the vibrator is expected to complete any
* previous vibration and turn off. This is used to allow this step to
* be triggered when the completion callback is received, and can
* be used to play effects back-to-back.
*/
AbstractVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller, VibrationEffect.Composed effect, int index,
long previousStepVibratorOffTimeout) {
super(conductor, startTime);
this.controller = controller;
this.effect = effect;
this.segmentIndex = index;
this.previousStepVibratorOffTimeout = previousStepVibratorOffTimeout;
}
public int getVibratorId() {
return controller.getVibratorInfo().getId();
}
@Override
public long getVibratorOnDuration() {
return mVibratorOnResult;
}
@Override
public boolean acceptVibratorCompleteCallback(int vibratorId) {
boolean isSameVibrator = controller.getVibratorInfo().getId() == vibratorId;
mVibratorCompleteCallbackReceived |= isSameVibrator;
// Only activate this step if a timeout was set to wait for the vibration to complete,
// otherwise we are waiting for the correct time to play the next step.
return isSameVibrator && (previousStepVibratorOffTimeout > SystemClock.uptimeMillis());
}
@Override
public List<Step> cancel() {
return Arrays.asList(new CompleteEffectVibratorStep(conductor, SystemClock.uptimeMillis(),
/* cancelled= */ true, controller, previousStepVibratorOffTimeout));
}
@Override
public void cancelImmediately() {
if (previousStepVibratorOffTimeout > SystemClock.uptimeMillis()) {
// Vibrator might be running from previous steps, so turn it off while canceling.
stopVibrating();
}
}
protected void stopVibrating() {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Turning off vibrator " + getVibratorId());
}
controller.off();
}
protected void changeAmplitude(float amplitude) {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Amplitude changed on vibrator " + getVibratorId() + " to " + amplitude);
}
controller.setAmplitude(amplitude);
}
/**
* Return the {@link VibrationStepConductor#nextVibrateStep} with same timings, only jumping
* the segments.
*/
protected List<Step> skipToNextSteps(int segmentsSkipped) {
return nextSteps(startTime, previousStepVibratorOffTimeout, segmentsSkipped);
}
/**
* Return the {@link VibrationStepConductor#nextVibrateStep} with same start and off timings
* calculated from {@link #getVibratorOnDuration()}, jumping all played segments.
*
* <p>This method has same behavior as {@link #skipToNextSteps(int)} when the vibrator
* result is non-positive, meaning the vibrator has either ignored or failed to turn on.
*/
protected List<Step> nextSteps(int segmentsPlayed) {
if (mVibratorOnResult <= 0) {
// Vibration was not started, so just skip the played segments and keep timings.
return skipToNextSteps(segmentsPlayed);
}
long nextStartTime = SystemClock.uptimeMillis() + mVibratorOnResult;
long nextVibratorOffTimeout =
nextStartTime + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
return nextSteps(nextStartTime, nextVibratorOffTimeout, segmentsPlayed);
}
/**
* Return the {@link VibrationStepConductor#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.
*/
protected List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout,
int segmentsPlayed) {
Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect,
segmentIndex + segmentsPlayed, vibratorOffTimeout);
return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep);
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationEffect;
import android.util.Slog;
import java.util.Arrays;
import java.util.List;
/**
* Represents a step to complete a {@link VibrationEffect}.
*
* <p>This runs right at the time the vibration is considered to end and will update the pending
* vibrators count. This can turn off the vibrator or slowly ramp it down to zero amplitude.
*/
final class CompleteEffectVibratorStep extends AbstractVibratorStep {
private final boolean mCancelled;
CompleteEffectVibratorStep(VibrationStepConductor conductor, long startTime, boolean cancelled,
VibratorController controller, long previousStepVibratorOffTimeout) {
super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
previousStepVibratorOffTimeout);
mCancelled = cancelled;
}
@Override
public boolean isCleanUp() {
// If the vibration was cancelled then this is just a clean up to ramp off the vibrator.
// Otherwise this step is part of the vibration.
return mCancelled;
}
@Override
public List<Step> cancel() {
if (mCancelled) {
// Double cancelling will just turn off the vibrator right away.
return Arrays.asList(
new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller));
}
return super.cancel();
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "CompleteEffectVibratorStep");
try {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Running " + (mCancelled ? "cancel" : "complete") + " vibration"
+ " step on vibrator " + controller.getVibratorInfo().getId());
}
if (mVibratorCompleteCallbackReceived) {
// Vibration completion callback was received by this step, just turn if off
// and skip any clean-up.
stopVibrating();
return VibrationStepConductor.EMPTY_STEP_LIST;
}
float currentAmplitude = controller.getCurrentAmplitude();
long remainingOnDuration =
previousStepVibratorOffTimeout - VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT
- SystemClock.uptimeMillis();
long rampDownDuration =
Math.min(remainingOnDuration,
conductor.vibrationSettings.getRampDownDuration());
long stepDownDuration = conductor.vibrationSettings.getRampStepDuration();
if (currentAmplitude < VibrationStepConductor.RAMP_OFF_AMPLITUDE_MIN
|| rampDownDuration <= stepDownDuration) {
// No need to ramp down the amplitude, just wait to turn it off.
if (mCancelled) {
// Vibration is completing because it was cancelled, turn off right away.
stopVibrating();
return VibrationStepConductor.EMPTY_STEP_LIST;
} else {
return Arrays.asList(new TurnOffVibratorStep(
conductor, previousStepVibratorOffTimeout, controller));
}
}
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Ramping down vibrator " + controller.getVibratorInfo().getId()
+ " from amplitude " + currentAmplitude
+ " for " + rampDownDuration + "ms");
}
float amplitudeDelta = currentAmplitude / (rampDownDuration / stepDownDuration);
float amplitudeTarget = currentAmplitude - amplitudeDelta;
long newVibratorOffTimeout =
mCancelled ? rampDownDuration : previousStepVibratorOffTimeout;
return Arrays.asList(
new RampOffVibratorStep(conductor, startTime, amplitudeTarget, amplitudeDelta,
controller, newVibratorOffTimeout));
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a step to 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.
*/
final class ComposePrimitivesVibratorStep extends AbstractVibratorStep {
ComposePrimitivesVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller, VibrationEffect.Composed effect, int index,
long previousStepVibratorOffTimeout) {
// This step should wait for the last vibration to finish (with the timeout) and for the
// intended step start time (to respect the effect delays).
super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
index, previousStepVibratorOffTimeout);
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep");
try {
// Load the next PrimitiveSegments to create a single compose call to the vibrator,
// limited to the vibrator composition maximum size.
int limit = controller.getVibratorInfo().getCompositionSizeMax();
int segmentCount = limit > 0
? Math.min(effect.getSegments().size(), segmentIndex + limit)
: effect.getSegments().size();
List<PrimitiveSegment> primitives = new ArrayList<>();
for (int i = segmentIndex; i < segmentCount; i++) {
VibrationEffectSegment segment = effect.getSegments().get(i);
if (segment instanceof PrimitiveSegment) {
primitives.add((PrimitiveSegment) segment);
} else {
break;
}
}
if (primitives.isEmpty()) {
Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePrimitivesStep: "
+ effect.getSegments().get(segmentIndex));
return skipToNextSteps(/* segmentsSkipped= */ 1);
}
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG, "Compose " + primitives + " primitives on vibrator "
+ controller.getVibratorInfo().getId());
}
mVibratorOnResult = controller.on(
primitives.toArray(new PrimitiveSegment[primitives.size()]),
getVibration().id);
return nextSteps(/* segmentsPlayed= */ primitives.size());
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a step to 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.
*/
final class ComposePwleVibratorStep extends AbstractVibratorStep {
ComposePwleVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller, VibrationEffect.Composed effect, int index,
long previousStepVibratorOffTimeout) {
// This step should wait for the last vibration to finish (with the timeout) and for the
// intended step start time (to respect the effect delays).
super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
index, previousStepVibratorOffTimeout);
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep");
try {
// Load the next RampSegments to create a single composePwle call to the vibrator,
// limited to the vibrator PWLE maximum size.
int limit = controller.getVibratorInfo().getPwleSizeMax();
int segmentCount = limit > 0
? Math.min(effect.getSegments().size(), segmentIndex + limit)
: effect.getSegments().size();
List<RampSegment> pwles = new ArrayList<>();
for (int i = segmentIndex; i < segmentCount; i++) {
VibrationEffectSegment segment = effect.getSegments().get(i);
if (segment instanceof RampSegment) {
pwles.add((RampSegment) segment);
} else {
break;
}
}
if (pwles.isEmpty()) {
Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePwleStep: "
+ effect.getSegments().get(segmentIndex));
return skipToNextSteps(/* segmentsSkipped= */ 1);
}
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator "
+ controller.getVibratorInfo().getId());
}
mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]),
getVibration().id);
return nextSteps(/* segmentsPlayed= */ pwles.size());
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.Trace;
import android.util.Slog;
import java.util.Arrays;
import java.util.List;
/**
* Finish a sync vibration started by a {@link StartSequentialEffectStep}.
*
* <p>This only plays after all active vibrators steps have finished, and adds a {@link
* StartSequentialEffectStep} to the queue if the sequential effect isn't finished yet.
*/
final class FinishSequentialEffectStep extends Step {
public final StartSequentialEffectStep startedStep;
FinishSequentialEffectStep(StartSequentialEffectStep startedStep) {
// No predefined startTime, just wait for all steps in the queue.
super(startedStep.conductor, Long.MAX_VALUE);
this.startedStep = startedStep;
}
@Override
public boolean isCleanUp() {
// This step only notes that all the vibrators has been turned off.
return true;
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "FinishSequentialEffectStep");
try {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"FinishSequentialEffectStep for effect #" + startedStep.currentIndex);
}
conductor.vibratorManagerHooks.noteVibratorOff(conductor.getVibration().uid);
Step nextStep = startedStep.nextStep();
return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST
: Arrays.asList(nextStep);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
@Override
public List<Step> cancel() {
cancelImmediately();
return VibrationStepConductor.EMPTY_STEP_LIST;
}
@Override
public void cancelImmediately() {
conductor.vibratorManagerHooks.noteVibratorOff(conductor.getVibration().uid);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a step to 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.
*/
final class PerformPrebakedVibratorStep extends AbstractVibratorStep {
PerformPrebakedVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller, VibrationEffect.Composed effect, int index,
long previousStepVibratorOffTimeout) {
// This step should wait for the last vibration to finish (with the timeout) and for the
// intended step start time (to respect the effect delays).
super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
index, previousStepVibratorOffTimeout);
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformPrebakedVibratorStep");
try {
VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
if (!(segment instanceof PrebakedSegment)) {
Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a "
+ "PerformPrebakedVibratorStep: " + segment);
return skipToNextSteps(/* segmentsSkipped= */ 1);
}
PrebakedSegment prebaked = (PrebakedSegment) segment;
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG, "Perform " + VibrationEffect.effectIdToString(
prebaked.getEffectId()) + " on vibrator "
+ controller.getVibratorInfo().getId());
}
VibrationEffect fallback = getVibration().getFallback(prebaked.getEffectId());
mVibratorOnResult = controller.on(prebaked, getVibration().id);
if (mVibratorOnResult == 0 && prebaked.shouldFallback()
&& (fallback instanceof VibrationEffect.Composed)) {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG, "Playing fallback for effect "
+ VibrationEffect.effectIdToString(prebaked.getEffectId()));
}
AbstractVibratorStep fallbackStep = conductor.nextVibrateStep(startTime, controller,
replaceCurrentSegment((VibrationEffect.Composed) fallback),
segmentIndex, previousStepVibratorOffTimeout);
List<Step> fallbackResult = fallbackStep.play();
// Update the result with the fallback result so this step is seamlessly
// replaced by the fallback to any outer application of this.
mVibratorOnResult = fallbackStep.getVibratorOnDuration();
return fallbackResult;
}
return nextSteps(/* segmentsPlayed= */ 1);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
/**
* 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() - 1;
}
return new VibrationEffect.Composed(newSegments, newRepeatIndex);
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.SystemClock;
import android.os.Trace;
import android.util.Slog;
import java.util.Arrays;
import java.util.List;
/** Represents a step to ramp down the vibrator amplitude before turning it off. */
final class RampOffVibratorStep extends AbstractVibratorStep {
private final float mAmplitudeTarget;
private final float mAmplitudeDelta;
RampOffVibratorStep(VibrationStepConductor conductor, long startTime, float amplitudeTarget,
float amplitudeDelta, VibratorController controller,
long previousStepVibratorOffTimeout) {
super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
previousStepVibratorOffTimeout);
mAmplitudeTarget = amplitudeTarget;
mAmplitudeDelta = amplitudeDelta;
}
@Override
public boolean isCleanUp() {
return true;
}
@Override
public List<Step> cancel() {
return Arrays.asList(
new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller));
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffVibratorStep");
try {
if (VibrationThread.DEBUG) {
long latency = SystemClock.uptimeMillis() - startTime;
Slog.d(VibrationThread.TAG, "Ramp down the vibrator amplitude, step with "
+ latency + "ms latency.");
}
if (mVibratorCompleteCallbackReceived) {
// Vibration completion callback was received by this step, just turn if off
// and skip the rest of the steps to ramp down the vibrator amplitude.
stopVibrating();
return VibrationStepConductor.EMPTY_STEP_LIST;
}
changeAmplitude(mAmplitudeTarget);
float newAmplitudeTarget = mAmplitudeTarget - mAmplitudeDelta;
if (newAmplitudeTarget < VibrationStepConductor.RAMP_OFF_AMPLITUDE_MIN) {
// Vibrator amplitude cannot go further down, just turn it off.
return Arrays.asList(new TurnOffVibratorStep(
conductor, previousStepVibratorOffTimeout, controller));
}
return Arrays.asList(new RampOffVibratorStep(
conductor,
startTime + conductor.vibrationSettings.getRampStepDuration(),
newAmplitudeTarget, mAmplitudeDelta, controller,
previousStepVibratorOffTimeout));
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
import java.util.Arrays;
import java.util.List;
/**
* 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}.
*/
final class SetAmplitudeVibratorStep extends AbstractVibratorStep {
private long mNextOffTime;
SetAmplitudeVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller, VibrationEffect.Composed effect, int index,
long previousStepVibratorOffTimeout) {
// This step has a fixed startTime coming from the timings of the waveform it's playing.
super(conductor, startTime, controller, effect, index, previousStepVibratorOffTimeout);
mNextOffTime = previousStepVibratorOffTimeout;
}
@Override
public boolean acceptVibratorCompleteCallback(int vibratorId) {
if (controller.getVibratorInfo().getId() == vibratorId) {
mVibratorCompleteCallbackReceived = true;
mNextOffTime = SystemClock.uptimeMillis();
}
// Timings are tightly controlled here, so only trigger this step if the vibrator was
// supposed to be ON but has completed prematurely, to turn it back on as soon as
// possible.
return mNextOffTime < startTime && controller.getCurrentAmplitude() > 0;
}
@Override
public List<Step> play() {
// TODO: consider separating the "on" steps at the start into a separate Step.
// TODO: consider instantiating the step with the required amplitude, rather than
// needing to dig into the effect.
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SetAmplitudeVibratorStep");
try {
long now = SystemClock.uptimeMillis();
long latency = now - startTime;
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Running amplitude step with " + latency + "ms latency.");
}
if (mVibratorCompleteCallbackReceived && latency < 0) {
// This step was run early because the vibrator turned off prematurely.
// Turn it back on and return this same step to run at the exact right time.
mNextOffTime = turnVibratorBackOn(/* remainingDuration= */ -latency);
return Arrays.asList(new SetAmplitudeVibratorStep(conductor, startTime, controller,
effect, segmentIndex, mNextOffTime));
}
VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
if (!(segment instanceof StepSegment)) {
Slog.w(VibrationThread.TAG,
"Ignoring wrong segment for a SetAmplitudeVibratorStep: " + segment);
return skipToNextSteps(/* segmentsSkipped= */ 1);
}
StepSegment stepSegment = (StepSegment) segment;
if (stepSegment.getDuration() == 0) {
// Skip waveform entries with zero timing.
return skipToNextSteps(/* segmentsSkipped= */ 1);
}
float amplitude = stepSegment.getAmplitude();
if (amplitude == 0) {
if (previousStepVibratorOffTimeout > now) {
// Amplitude cannot be set to zero, so stop the vibrator.
stopVibrating();
mNextOffTime = now;
}
} else {
if (startTime >= mNextOffTime) {
// Vibrator is OFF. Turn vibrator back on for the duration of another
// cycle before setting the amplitude.
long onDuration = getVibratorOnDuration(effect, segmentIndex);
if (onDuration > 0) {
mVibratorOnResult = startVibrating(onDuration);
mNextOffTime = now + onDuration
+ VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
}
}
changeAmplitude(amplitude);
}
// 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);
}
}
private long turnVibratorBackOn(long remainingDuration) {
long onDuration = getVibratorOnDuration(effect, segmentIndex);
if (onDuration <= 0) {
// Vibrator is supposed to go back off when this step starts, so just leave it off.
return previousStepVibratorOffTimeout;
}
onDuration += remainingDuration;
float expectedAmplitude = controller.getCurrentAmplitude();
mVibratorOnResult = startVibrating(onDuration);
if (mVibratorOnResult > 0) {
// Set the amplitude back to the value it was supposed to be playing at.
changeAmplitude(expectedAmplitude);
}
return SystemClock.uptimeMillis() + onDuration
+ VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
}
private long startVibrating(long duration) {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Turning on vibrator " + controller.getVibratorInfo().getId() + " for "
+ duration + "ms");
}
return controller.on(duration, getVibration().id);
}
/**
* 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) {
// The repeating waveform keeps the vibrator ON all the time. Use a minimum
// of 1s duration to prevent short patterns from turning the vibrator ON too
// frequently.
return Math.max(timing, 1000);
}
}
if (i == segmentCount && effect.getRepeatIndex() < 0) {
// Vibration ending at non-zero amplitude, add extra timings to ramp down after
// vibration is complete.
timing += conductor.vibrationSettings.getRampDownDuration();
}
return timing;
}
}

View File

@@ -0,0 +1,371 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.annotation.Nullable;
import android.hardware.vibrator.IVibratorManager;
import android.os.CombinedVibration;
import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.List;
/**
* Starts a sync vibration.
*
* <p>If this step has successfully started playing a vibration on any vibrator, it will always
* add a {@link FinishSequentialEffectStep} to the queue, to be played after all vibrators
* have finished all their individual steps.
*
* <p>If this step does not start any vibrator, it will add a {@link StartSequentialEffectStep} if
* the sequential effect isn't finished yet.
*
* <p>TODO: this step actually does several things: multiple HAL calls to sync the vibrators,
* as well as dispatching the underlying vibrator instruction calls (which need to be done before
* triggering the synced effects). This role/encapsulation could probably be improved to split up
* the grouped HAL calls here, as well as to clarify the role of dispatching VibratorSteps between
* this class and the controller.
*/
final class StartSequentialEffectStep extends Step {
public final CombinedVibration.Sequential sequentialEffect;
public final int currentIndex;
private long mVibratorsOnMaxDuration;
StartSequentialEffectStep(VibrationStepConductor conductor,
CombinedVibration.Sequential effect) {
this(conductor, SystemClock.uptimeMillis() + effect.getDelays().get(0), effect,
/* index= */ 0);
}
StartSequentialEffectStep(VibrationStepConductor conductor, long startTime,
CombinedVibration.Sequential effect, int index) {
super(conductor, startTime);
sequentialEffect = effect;
currentIndex = index;
}
@Override
public long getVibratorOnDuration() {
return mVibratorsOnMaxDuration;
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartSequentialEffectStep");
List<Step> nextSteps = new ArrayList<>();
mVibratorsOnMaxDuration = -1;
try {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"StartSequentialEffectStep for effect #" + currentIndex);
}
CombinedVibration effect = sequentialEffect.getEffects().get(currentIndex);
DeviceEffectMap effectMapping = createEffectToVibratorMapping(effect);
if (effectMapping == null) {
// Unable to map effects to vibrators, ignore this step.
return nextSteps;
}
mVibratorsOnMaxDuration = startVibrating(effectMapping, nextSteps);
if (mVibratorsOnMaxDuration > 0) {
conductor.vibratorManagerHooks.noteVibratorOn(conductor.getVibration().uid,
mVibratorsOnMaxDuration);
}
} finally {
if (mVibratorsOnMaxDuration >= 0) {
// 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 =
mVibratorsOnMaxDuration > 0 ? new FinishSequentialEffectStep(this)
: nextStep();
if (nextStep != null) {
nextSteps.add(nextStep);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
return nextSteps;
}
@Override
public List<Step> cancel() {
return VibrationStepConductor.EMPTY_STEP_LIST;
}
@Override
public void cancelImmediately() {
}
/**
* Create the next {@link StartSequentialEffectStep} to play this sequential effect, starting at
* the
* time this method is called, or null if sequence is complete.
*/
@Nullable
Step nextStep() {
int nextIndex = currentIndex + 1;
if (nextIndex >= sequentialEffect.getEffects().size()) {
return null;
}
long nextEffectDelay = sequentialEffect.getDelays().get(nextIndex);
long nextStartTime = SystemClock.uptimeMillis() + nextEffectDelay;
return new StartSequentialEffectStep(conductor, nextStartTime, sequentialEffect,
nextIndex);
}
/** Create a mapping of individual {@link VibrationEffect} to available vibrators. */
@Nullable
private DeviceEffectMap createEffectToVibratorMapping(
CombinedVibration effect) {
if (effect instanceof CombinedVibration.Mono) {
return new DeviceEffectMap((CombinedVibration.Mono) effect);
}
if (effect instanceof CombinedVibration.Stereo) {
return new DeviceEffectMap((CombinedVibration.Stereo) effect);
}
return null;
}
/**
* Starts playing effects on designated vibrators, in sync.
*
* @param effectMapping The {@link CombinedVibration} mapped to this device vibrators
* @param nextSteps An output list to accumulate the future {@link Step
* Steps} created
* by this method, typically one for each vibrator that has
* successfully started vibrating on this step.
* @return The duration, in millis, of the {@link CombinedVibration}. Repeating
* waveforms return {@link Long#MAX_VALUE}. Zero or negative values indicate the vibrators
* have ignored all effects.
*/
private long startVibrating(
DeviceEffectMap effectMapping, List<Step> nextSteps) {
int vibratorCount = effectMapping.size();
if (vibratorCount == 0) {
// No effect was mapped to any available vibrator.
return 0;
}
AbstractVibratorStep[] steps = new AbstractVibratorStep[vibratorCount];
long vibrationStartTime = SystemClock.uptimeMillis();
for (int i = 0; i < vibratorCount; i++) {
steps[i] = conductor.nextVibrateStep(vibrationStartTime,
conductor.getVibrators().get(effectMapping.vibratorIdAt(i)),
effectMapping.effectAt(i),
/* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0);
}
if (steps.length == 1) {
// No need to prepare and trigger sync effects on a single vibrator.
return startVibrating(steps[0], nextSteps);
}
// This synchronization of vibrators should be executed one at a time, even if we are
// vibrating different sets of vibrators in parallel. The manager can only prepareSynced
// one set of vibrators at a time.
// This property is guaranteed by there only being one thread (VibrationThread) executing
// one Step at a time, so there's no need to hold the state lock.
// TODO: remove the large locked block in a dedicated change.
synchronized (conductor.mLock) {
boolean hasPrepared = false;
boolean hasTriggered = false;
long maxDuration = 0;
try {
hasPrepared = conductor.vibratorManagerHooks.prepareSyncedVibration(
effectMapping.getRequiredSyncCapabilities(),
effectMapping.getVibratorIds());
for (AbstractVibratorStep 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 && maxDuration > 0) {
hasTriggered = conductor.vibratorManagerHooks.triggerSyncedVibration(
getVibration().id);
}
return maxDuration;
} finally {
if (hasPrepared && !hasTriggered) {
// Trigger has failed or all steps were ignored by the vibrators.
conductor.vibratorManagerHooks.cancelSyncedVibration();
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).cancelImmediately();
}
}
}
}
}
private long startVibrating(AbstractVibratorStep step, List<Step> nextSteps) {
nextSteps.addAll(step.play());
long stepDuration = step.getVibratorOnDuration();
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());
}
/**
* Map a {@link CombinedVibration} to the vibrators available on the device.
*
* <p>This contains the logic to find the capabilities required from {@link IVibratorManager} to
* play all of the effects in sync.
*/
final class DeviceEffectMap {
private final SparseArray<VibrationEffect.Composed> mVibratorEffects;
private final int[] mVibratorIds;
private final long mRequiredSyncCapabilities;
DeviceEffectMap(CombinedVibration.Mono mono) {
SparseArray<VibratorController> vibrators = conductor.getVibrators();
mVibratorEffects = new SparseArray<>(vibrators.size());
mVibratorIds = new int[vibrators.size()];
for (int i = 0; i < vibrators.size(); i++) {
int vibratorId = vibrators.keyAt(i);
VibratorInfo vibratorInfo = vibrators.valueAt(i).getVibratorInfo();
VibrationEffect effect = conductor.deviceEffectAdapter.apply(
mono.getEffect(), vibratorInfo);
if (effect instanceof VibrationEffect.Composed) {
mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect);
mVibratorIds[i] = vibratorId;
}
}
mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects);
}
DeviceEffectMap(CombinedVibration.Stereo stereo) {
SparseArray<VibratorController> vibrators = conductor.getVibrators();
SparseArray<VibrationEffect> stereoEffects = stereo.getEffects();
mVibratorEffects = new SparseArray<>();
for (int i = 0; i < stereoEffects.size(); i++) {
int vibratorId = stereoEffects.keyAt(i);
if (vibrators.contains(vibratorId)) {
VibratorInfo vibratorInfo = vibrators.valueAt(i).getVibratorInfo();
VibrationEffect effect = conductor.deviceEffectAdapter.apply(
stereoEffects.valueAt(i), vibratorInfo);
if (effect instanceof VibrationEffect.Composed) {
mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect);
}
}
}
mVibratorIds = new int[mVibratorEffects.size()];
for (int i = 0; i < mVibratorEffects.size(); i++) {
mVibratorIds[i] = mVibratorEffects.keyAt(i);
}
mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects);
}
/**
* Return the number of vibrators mapped to play the {@link CombinedVibration} on this
* device.
*/
public int size() {
return mVibratorIds.length;
}
/**
* Return all capabilities required to play the {@link CombinedVibration} in
* between calls to {@link IVibratorManager#prepareSynced} and
* {@link IVibratorManager#triggerSynced}.
*/
public long getRequiredSyncCapabilities() {
return mRequiredSyncCapabilities;
}
/** Return all vibrator ids mapped to play the {@link CombinedVibration}. */
public int[] getVibratorIds() {
return mVibratorIds;
}
/** Return the id of the vibrator at given index. */
public int vibratorIdAt(int index) {
return mVibratorEffects.keyAt(index);
}
/** Return the {@link VibrationEffect} at given index. */
public VibrationEffect.Composed effectAt(int index) {
return mVibratorEffects.valueAt(index);
}
/**
* Return all capabilities required from the {@link IVibratorManager} to prepare and
* trigger all given effects in sync.
*
* @return {@link IVibratorManager#CAP_SYNC} together with all required
* IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities.
*/
private long calculateRequiredSyncCapabilities(
SparseArray<VibrationEffect.Composed> effects) {
long prepareCap = 0;
for (int i = 0; i < effects.size(); i++) {
VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0);
if (firstSegment instanceof StepSegment) {
prepareCap |= IVibratorManager.CAP_PREPARE_ON;
} else if (firstSegment instanceof PrebakedSegment) {
prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM;
} else if (firstSegment instanceof PrimitiveSegment) {
prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE;
}
}
int triggerCap = 0;
if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_ON)) {
triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_ON;
}
if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_PERFORM)) {
triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
}
if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_COMPOSE)) {
triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
}
return IVibratorManager.CAP_SYNC | prepareCap | triggerCap;
}
/**
* Return true if {@code prepareCapabilities} contains this {@code capability} mixed with
* different ones, requiring a mixed trigger capability from the vibrator manager for
* syncing all effects.
*/
private boolean requireMixedTriggerCapability(long prepareCapabilities, long capability) {
return (prepareCapabilities & capability) != 0
&& (prepareCapabilities & ~capability) != 0;
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.annotation.NonNull;
import android.os.CombinedVibration;
import android.os.SystemClock;
import android.os.VibrationEffect;
import java.util.List;
/**
* Represent a single step for playing a vibration.
*
* <p>Every step has a start time, which can be used to apply delays between steps while
* executing them in sequence.
*/
abstract class Step implements Comparable<Step> {
public final VibrationStepConductor conductor;
public final long startTime;
Step(VibrationStepConductor conductor, long startTime) {
this.conductor = conductor;
this.startTime = startTime;
}
protected Vibration getVibration() {
return conductor.getVibration();
}
/**
* Returns true if this step is a clean up step and not part of a {@link VibrationEffect} or
* {@link CombinedVibration}.
*/
public boolean isCleanUp() {
return false;
}
/** Play this step, returning a (possibly empty) list of next steps. */
@NonNull
public abstract List<Step> play();
/**
* Cancel this pending step and return a (possibly empty) list of clean-up steps that should
* be played to gracefully cancel this step.
*/
@NonNull
public abstract List<Step> cancel();
/** Cancel this pending step immediately, skipping any clean-up. */
public abstract void cancelImmediately();
/**
* Return the duration the vibrator was turned on when this step was played.
*
* @return A positive duration that the vibrator was turned on for by this step;
* Zero if the segment is not supported, the step was not played yet or vibrator was never
* turned on by this step; A negative value if the vibrator call has failed.
*/
public long getVibratorOnDuration() {
return 0;
}
/**
* Return true to run this step right after a vibrator has notified vibration completed,
* used to resume steps waiting on vibrator callbacks with a timeout.
*/
public boolean acceptVibratorCompleteCallback(int vibratorId) {
return false;
}
/**
* Returns the time in millis to wait before playing this step. This is performed
* while holding the queue lock, so should not rely on potentially slow operations.
*/
public long calculateWaitTime() {
if (startTime == Long.MAX_VALUE) {
// This step don't have a predefined start time, it's just marked to be executed
// after all other steps have finished.
return 0;
}
return Math.max(0, startTime - SystemClock.uptimeMillis());
}
@Override
public int compareTo(Step o) {
return Long.compare(startTime, o.startTime);
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.os.SystemClock;
import android.os.Trace;
import java.util.Arrays;
import java.util.List;
/**
* Represents a step to turn the vibrator off.
*
* <p>This runs after a timeout on the expected time the vibrator should have finished playing,
* and can be brought forward by vibrator complete callbacks. The step shouldn't be skipped, even
* if the vibrator-complete callback was received, as some implementations still rely on the
* "off" call to actually stop.
*/
final class TurnOffVibratorStep extends AbstractVibratorStep {
TurnOffVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller) {
super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, startTime);
}
@Override
public boolean isCleanUp() {
return true;
}
@Override
public List<Step> cancel() {
return Arrays.asList(
new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller));
}
@Override
public void cancelImmediately() {
stopVibrating();
}
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "TurnOffVibratorStep");
try {
stopVibrating();
return VibrationStepConductor.EMPTY_STEP_LIST;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
}

View File

@@ -0,0 +1,355 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.vibrator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.CombinedVibration;
import android.os.VibrationEffect;
import android.os.WorkSource;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
/**
* Creates and manages a queue of steps for performing a VibrationEffect, as well as coordinating
* dispatch of callbacks.
*/
final class VibrationStepConductor {
/**
* Extra timeout added to the end of each vibration step to ensure it finishes even when
* vibrator callbacks are lost.
*/
static final long CALLBACKS_EXTRA_TIMEOUT = 1_000;
/** Threshold to prevent the ramp off steps from trying to set extremely low amplitudes. */
static final float RAMP_OFF_AMPLITUDE_MIN = 1e-3f;
static final List<Step> EMPTY_STEP_LIST = new ArrayList<>();
final Object mLock = new Object();
// Used within steps.
public final VibrationSettings vibrationSettings;
public final DeviceVibrationEffectAdapter deviceEffectAdapter;
public final VibrationThread.VibratorManagerHooks vibratorManagerHooks;
private final WorkSource mWorkSource;
private final Vibration mVibration;
private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
@GuardedBy("mLock")
private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>();
@GuardedBy("mLock")
private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>();
@GuardedBy("mLock")
private final Queue<Integer> mCompletionNotifiedVibrators = new LinkedList<>();
@GuardedBy("mLock")
private int mPendingVibrateSteps;
@GuardedBy("mLock")
private int mConsumedStartVibrateSteps;
@GuardedBy("mLock")
private int mSuccessfulVibratorOnSteps;
@GuardedBy("mLock")
private boolean mWaitToProcessVibratorCompleteCallbacks;
VibrationStepConductor(Vibration vib, VibrationSettings vibrationSettings,
DeviceVibrationEffectAdapter effectAdapter,
SparseArray<VibratorController> availableVibrators,
VibrationThread.VibratorManagerHooks vibratorManagerHooks) {
this.mVibration = vib;
this.vibrationSettings = vibrationSettings;
this.deviceEffectAdapter = effectAdapter;
this.vibratorManagerHooks = vibratorManagerHooks;
this.mWorkSource = new WorkSource(mVibration.uid);
CombinedVibration effect = vib.getEffect();
for (int i = 0; i < availableVibrators.size(); i++) {
if (effect.hasVibrator(availableVibrators.keyAt(i))) {
mVibrators.put(availableVibrators.keyAt(i), availableVibrators.valueAt(i));
}
}
}
@Nullable
AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller,
VibrationEffect.Composed effect, int segmentIndex,
long previousStepVibratorOffTimeout) {
if (segmentIndex >= effect.getSegments().size()) {
segmentIndex = effect.getRepeatIndex();
}
if (segmentIndex < 0) {
// No more segments to play, last step is to complete the vibration on this vibrator.
return new CompleteEffectVibratorStep(this, startTime, /* cancelled= */ false,
controller, previousStepVibratorOffTimeout);
}
VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
if (segment instanceof PrebakedSegment) {
return new PerformPrebakedVibratorStep(this, startTime, controller, effect,
segmentIndex, previousStepVibratorOffTimeout);
}
if (segment instanceof PrimitiveSegment) {
return new ComposePrimitivesVibratorStep(this, startTime, controller, effect,
segmentIndex, previousStepVibratorOffTimeout);
}
if (segment instanceof RampSegment) {
return new ComposePwleVibratorStep(this, startTime, controller, effect, segmentIndex,
previousStepVibratorOffTimeout);
}
return new SetAmplitudeVibratorStep(this, startTime, controller, effect, segmentIndex,
previousStepVibratorOffTimeout);
}
public void initializeForEffect(@NonNull CombinedVibration.Sequential vibration) {
synchronized (mLock) {
mPendingVibrateSteps++;
mNextSteps.offer(new StartSequentialEffectStep(this, vibration));
}
}
public Vibration getVibration() {
return mVibration;
}
public WorkSource getWorkSource() {
return mWorkSource;
}
SparseArray<VibratorController> getVibrators() {
return mVibrators;
}
public boolean isFinished() {
synchronized (mLock) {
return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty();
}
}
/**
* Calculate the {@link Vibration.Status} based on the current queue state and the expected
* number of {@link StartSequentialEffectStep} to be played.
*/
public Vibration.Status calculateVibrationStatus(int expectedStartVibrateSteps) {
synchronized (mLock) {
if (mPendingVibrateSteps > 0
|| mConsumedStartVibrateSteps < expectedStartVibrateSteps) {
return Vibration.Status.RUNNING;
}
if (mSuccessfulVibratorOnSteps > 0) {
return Vibration.Status.FINISHED;
}
// If no step was able to turn the vibrator ON successfully.
return Vibration.Status.IGNORED_UNSUPPORTED;
}
}
/** Returns the time in millis to wait before calling {@link #runNextStep()}. */
@GuardedBy("mLock")
public long getWaitMillisBeforeNextStepLocked() {
if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
// Steps resumed by vibrator complete callback should be played right away.
return 0;
}
Step nextStep = mNextSteps.peek();
return nextStep == null ? 0 : nextStep.calculateWaitTime();
}
/**
* Play and remove the step at the top of this queue, and also adds the next steps generated
* to be played next.
*/
public void runNextStep() {
// Vibrator callbacks should wait until the polled step is played and the next steps are
// added back to the queue, so they can handle the callback.
markWaitToProcessVibratorCallbacks();
try {
Step nextStep = pollNext();
if (nextStep != null) {
// This might turn on the vibrator and have a HAL latency. Execute this outside
// any lock to avoid blocking other interactions with the thread.
List<Step> nextSteps = nextStep.play();
synchronized (mLock) {
if (nextStep.getVibratorOnDuration() > 0) {
mSuccessfulVibratorOnSteps++;
}
if (nextStep instanceof StartSequentialEffectStep) {
mConsumedStartVibrateSteps++;
}
if (!nextStep.isCleanUp()) {
mPendingVibrateSteps--;
}
for (int i = 0; i < nextSteps.size(); i++) {
mPendingVibrateSteps += nextSteps.get(i).isCleanUp() ? 0 : 1;
}
mNextSteps.addAll(nextSteps);
}
}
} finally {
synchronized (mLock) {
processVibratorCompleteCallbacksLocked();
}
}
}
/**
* Notify the vibrator completion.
*
* <p>This is a lightweight method that do not trigger any operation from {@link
* VibratorController}, so it can be called directly from a native callback.
*/
@GuardedBy("mLock")
private void notifyVibratorCompleteLocked(int vibratorId) {
mCompletionNotifiedVibrators.offer(vibratorId);
if (!mWaitToProcessVibratorCompleteCallbacks) {
// No step is being played or cancelled now, process the callback right away.
processVibratorCompleteCallbacksLocked();
}
}
public void notifyVibratorComplete(int vibratorId) {
synchronized (mLock) {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Vibration complete reported by vibrator " + vibratorId);
}
notifyVibratorCompleteLocked(vibratorId);
mLock.notify();
}
}
public void notifySyncedVibrationComplete() {
synchronized (mLock) {
if (VibrationThread.DEBUG) {
Slog.d(VibrationThread.TAG,
"Synced vibration complete reported by vibrator manager");
}
for (int i = 0; i < mVibrators.size(); i++) {
notifyVibratorCompleteLocked(mVibrators.keyAt(i));
}
mLock.notify();
}
}
/**
* Cancel the current queue, replacing all remaining steps with respective clean-up steps.
*
* <p>This will remove all steps and replace them with respective
* {@link Step#cancel()}.
*/
public void cancel() {
// Vibrator callbacks should wait until all steps from the queue are properly cancelled
// and clean up steps are added back to the queue, so they can handle the callback.
markWaitToProcessVibratorCallbacks();
try {
List<Step> cleanUpSteps = new ArrayList<>();
Step step;
while ((step = pollNext()) != null) {
cleanUpSteps.addAll(step.cancel());
}
synchronized (mLock) {
// All steps generated by Step.cancel() should be clean-up steps.
mPendingVibrateSteps = 0;
mNextSteps.addAll(cleanUpSteps);
}
} finally {
synchronized (mLock) {
processVibratorCompleteCallbacksLocked();
}
}
}
/**
* Cancel the current queue immediately, clearing all remaining steps and skipping clean-up.
*
* <p>This will remove and trigger {@link Step#cancelImmediately()} in all steps, in order.
*/
public void cancelImmediately() {
// Vibrator callbacks should wait until all steps from the queue are properly cancelled.
markWaitToProcessVibratorCallbacks();
try {
Step step;
while ((step = pollNext()) != null) {
// This might turn off the vibrator and have a HAL latency. Execute this outside
// any lock to avoid blocking other interactions with the thread.
step.cancelImmediately();
}
synchronized (mLock) {
mPendingVibrateSteps = 0;
}
} finally {
synchronized (mLock) {
processVibratorCompleteCallbacksLocked();
}
}
}
@Nullable
private Step pollNext() {
synchronized (mLock) {
// Prioritize the steps resumed by a vibrator complete callback.
if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
return mPendingOnVibratorCompleteSteps.poll();
}
return mNextSteps.poll();
}
}
private void markWaitToProcessVibratorCallbacks() {
synchronized (mLock) {
mWaitToProcessVibratorCompleteCallbacks = true;
}
}
/**
* Notify the step in this queue that should be resumed by the vibrator completion
* callback and keep it separate to be consumed by {@link #runNextStep()}.
*
* <p>This is a lightweight method that do not trigger any operation from {@link
* VibratorController}, so it can be called directly from a native callback.
*
* <p>This assumes only one of the next steps is waiting on this given vibrator, so the
* first step found will be resumed by this method, in no particular order.
*/
@GuardedBy("mLock")
private void processVibratorCompleteCallbacksLocked() {
mWaitToProcessVibratorCompleteCallbacks = false;
while (!mCompletionNotifiedVibrators.isEmpty()) {
int vibratorId = mCompletionNotifiedVibrators.poll();
Iterator<Step> it = mNextSteps.iterator();
while (it.hasNext()) {
Step step = it.next();
if (step.acceptVibratorCompleteCallback(vibratorId)) {
it.remove();
mPendingOnVibratorCompleteSteps.offer(step);
break;
}
}
}
}
}

View File

@@ -42,6 +42,7 @@ import android.os.IVibratorStateListener;
import android.os.Looper;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
@@ -60,6 +61,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
import com.android.server.SystemService;
@@ -88,6 +90,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
| VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
/** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
/** Lifecycle responsible for initializing this class at the right system server phases. */
public static class Lifecycle extends SystemService {
private VibratorManagerService mService;
@@ -201,8 +206,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class)
.getSystemUiServiceComponent().getPackageName();
mBatteryStatsService = IBatteryStats.Stub.asInterface(ServiceManager.getService(
BatteryStats.SERVICE_NAME));
mBatteryStatsService = injector.getBatteryStatsService();
mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -634,7 +638,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
}
VibrationThread vibThread = new VibrationThread(vib, mVibrationSettings,
mDeviceVibrationEffectAdapter, mVibrators, mWakeLock, mBatteryStatsService,
mDeviceVibrationEffectAdapter, mVibrators, mWakeLock,
mVibrationThreadCallbacks);
if (mCurrentVibration == null) {
@@ -1105,6 +1109,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
return new Handler(looper);
}
IBatteryStats getBatteryStatsService() {
return IBatteryStats.Stub.asInterface(ServiceManager.getService(
BatteryStats.SERVICE_NAME));
}
VibratorController createVibratorController(int vibratorId,
VibratorController.OnVibrationCompleteListener listener) {
return new VibratorController(vibratorId, listener);
@@ -1140,6 +1149,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
mNativeWrapper.cancelSynced();
}
@Override
public void noteVibratorOn(int uid, long duration) {
try {
if (duration <= 0) {
return;
}
if (duration == Long.MAX_VALUE) {
// Repeating duration has started. Report a fixed duration here, noteVibratorOff
// should be called when this is cancelled.
duration = BATTERY_STATS_REPEATING_VIBRATION_DURATION;
}
mBatteryStatsService.noteVibratorOn(uid, duration);
FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON,
duration);
} catch (RemoteException e) {
}
}
@Override
public void noteVibratorOff(int uid) {
try {
mBatteryStatsService.noteVibratorOff(uid);
FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF,
/* duration= */ 0);
} catch (RemoteException e) {
}
}
@Override
public void onVibrationCompleted(long vibrationId, Vibration.Status status) {
if (DEBUG) {

View File

@@ -61,8 +61,6 @@ import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
import com.android.internal.app.IBatteryStats;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -106,8 +104,6 @@ public class VibrationThreadTest {
@Mock
private IBinder mVibrationToken;
@Mock
private IBatteryStats mIBatteryStatsMock;
@Mock
private VibrationConfig mVibrationConfigMock;
private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
@@ -178,8 +174,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -197,8 +193,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -219,8 +215,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(15L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -252,8 +248,8 @@ public class VibrationThreadTest {
thread.cancel();
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), anyLong());
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
verify(mManagerHooks).noteVibratorOff(eq(UID));
verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -404,8 +400,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -427,8 +423,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibration);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -446,8 +442,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong());
verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED);
assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments().isEmpty());
@@ -466,8 +462,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(40L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -486,8 +482,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong());
verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED);
assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments().isEmpty());
@@ -536,8 +532,8 @@ public class VibrationThreadTest {
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(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -573,8 +569,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(100L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -666,8 +662,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
@@ -690,8 +686,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
@@ -728,8 +724,8 @@ public class VibrationThreadTest {
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
@@ -777,13 +773,13 @@ public class VibrationThreadTest {
controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
InOrder batterVerifier = inOrder(mIBatteryStatsMock);
batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
InOrder batteryVerifier = inOrder(mManagerHooks);
batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
assertFalse(thread.getVibrators().get(1).isVibrating());
@@ -952,8 +948,8 @@ public class VibrationThreadTest {
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(80L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
@@ -1300,7 +1296,7 @@ public class VibrationThreadTest {
private VibrationThread startThreadAndDispatcher(Vibration vib) {
VibrationThread thread = new VibrationThread(vib, mVibrationSettings, mEffectAdapter,
createVibratorControllers(), mWakeLock, mIBatteryStatsMock, mManagerHooks);
createVibratorControllers(), mWakeLock, mManagerHooks);
doAnswer(answer -> {
thread.vibratorComplete(answer.getArgument(0));
return null;

View File

@@ -71,6 +71,7 @@ import android.os.VibratorInfo;
import android.os.test.TestLooper;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
@@ -78,6 +79,7 @@ import android.view.InputDevice;
import androidx.test.InstrumentationRegistry;
import com.android.internal.app.IBatteryStats;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.util.test.FakeSettingsProviderRule;
import com.android.server.LocalServices;
@@ -144,6 +146,8 @@ public class VibratorManagerServiceTest {
private AppOpsManager mAppOpsManagerMock;
@Mock
private IInputManager mIInputManagerMock;
@Mock
private IBatteryStats mBatteryStatsMock;
private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
@@ -152,12 +156,14 @@ public class VibratorManagerServiceTest {
private FakeVibrator mVibrator;
private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
private VibratorManagerService.ExternalVibratorService mExternalVibratorService;
private VibrationConfig mVibrationConfig;
@Before
public void setUp() throws Exception {
mTestLooper = new TestLooper();
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
InputManager inputManager = InputManager.resetInstance(mIInputManagerMock);
mVibrationConfig = new VibrationConfig(mContextSpy.getResources());
ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
@@ -221,6 +227,11 @@ public class VibratorManagerServiceTest {
return new Handler(mTestLooper.getLooper());
}
@Override
IBatteryStats getBatteryStatsService() {
return mBatteryStatsMock;
}
@Override
VibratorController createVibratorController(int vibratorId,
VibratorController.OnVibrationCompleteListener listener) {
@@ -382,6 +393,11 @@ public class VibratorManagerServiceTest {
inOrderVerifier.verify(listenerMock).onVibrating(eq(true));
inOrderVerifier.verify(listenerMock).onVibrating(eq(false));
inOrderVerifier.verifyNoMoreInteractions();
InOrder batteryVerifier = inOrder(mBatteryStatsMock);
batteryVerifier.verify(mBatteryStatsMock)
.noteVibratorOn(UID, 40 + mVibrationConfig.getRampDownDurationMs());
batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID);
}
@Test
@@ -731,6 +747,12 @@ public class VibratorManagerServiceTest {
// Wait before checking it never played a second effect.
assertFalse(waitUntil(s -> mVibratorProviders.get(1).getEffectSegments().size() > 1,
service, /* timeout= */ 50));
// The time estimate is recorded when the vibration starts, repeating vibrations
// are capped at BATTERY_STATS_REPEATING_VIBRATION_DURATION (=5000).
verify(mBatteryStatsMock).noteVibratorOn(UID, 5000);
// The second vibration shouldn't have recorded that the vibrators were turned on.
verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
}
@Test