summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/vibrator/AbstractVibratorStep.java148
-rw-r--r--services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java114
-rw-r--r--services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java84
-rw-r--r--services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java84
-rw-r--r--services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java73
-rw-r--r--services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java103
-rw-r--r--services/core/java/com/android/server/vibrator/RampOffVibratorStep.java84
-rw-r--r--services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java186
-rw-r--r--services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java371
-rw-r--r--services/core/java/com/android/server/vibrator/Step.java103
-rw-r--r--services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java66
-rw-r--r--services/core/java/com/android/server/vibrator/VibrationStepConductor.java355
-rw-r--r--services/core/java/com/android/server/vibrator/VibrationThread.java1457
-rw-r--r--services/core/java/com/android/server/vibrator/VibratorManagerService.java45
-rw-r--r--services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java80
-rw-r--r--services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java22
16 files changed, 1915 insertions, 1460 deletions
diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
new file mode 100644
index 000000000000..3550bda282dd
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
@@ -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);
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
new file mode 100644
index 000000000000..8585e3473ef3
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
@@ -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);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
new file mode 100644
index 000000000000..d1ea80557419
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
@@ -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);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
new file mode 100644
index 000000000000..73bf933f8bc9
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
@@ -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);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java
new file mode 100644
index 000000000000..bbbca024214f
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java
@@ -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);
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
new file mode 100644
index 000000000000..601ae978f637
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
@@ -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);
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
new file mode 100644
index 000000000000..8cf5fb394d9d
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
@@ -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);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
new file mode 100644
index 000000000000..d5c11161bdfa
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
@@ -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;
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java
new file mode 100644
index 000000000000..8ed002ae7ed3
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java
@@ -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;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/Step.java b/services/core/java/com/android/server/vibrator/Step.java
new file mode 100644
index 000000000000..042e8a0e6ad5
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/Step.java
@@ -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);
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java
new file mode 100644
index 000000000000..297ef5614e84
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java
@@ -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);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
new file mode 100644
index 000000000000..51691fbcdf5a
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 1f1f40b8121d..1b8e4c5bbb6b 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -16,59 +16,23 @@
package com.android.server.vibrator;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.hardware.vibrator.IVibratorManager;
import android.os.CombinedVibration;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
-import android.os.SystemClock;
import android.os.Trace;
-import android.os.VibrationEffect;
-import android.os.VibratorInfo;
-import android.os.WorkSource;
-import android.os.vibrator.PrebakedSegment;
-import android.os.vibrator.PrimitiveSegment;
-import android.os.vibrator.RampSegment;
-import android.os.vibrator.StepSegment;
-import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
import android.util.SparseArray;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.app.IBatteryStats;
-import com.android.internal.util.FrameworkStatsLog;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
import java.util.NoSuchElementException;
-import java.util.PriorityQueue;
-import java.util.Queue;
/** Plays a {@link Vibration} in dedicated thread. */
final class VibrationThread extends Thread implements IBinder.DeathRecipient {
- private static final String TAG = "VibrationThread";
- private static final boolean DEBUG = false;
-
- /**
- * Extra timeout added to the end of each vibration step to ensure it finishes even when
- * vibrator callbacks are lost.
- */
- private static final long CALLBACKS_EXTRA_TIMEOUT = 1_000;
-
- /** Threshold to prevent the ramp off steps from trying to set extremely low amplitudes. */
- private static final float RAMP_OFF_AMPLITUDE_MIN = 1e-3f;
-
- /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
- private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
-
- private static final List<Step> EMPTY_STEP_LIST = new ArrayList<>();
+ static final String TAG = "VibrationThread";
+ static final boolean DEBUG = false;
/** Calls into VibratorManager functionality needed for playing a {@link Vibration}. */
interface VibratorManagerHooks {
@@ -94,6 +58,15 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
void cancelSyncedVibration();
/**
+ * Record that a vibrator was turned on, and may remain on for the specified duration,
+ * on behalf of the given uid.
+ */
+ void noteVibratorOn(int uid, long duration);
+
+ /** Record that a vibrator was turned off, on behalf of the given uid. */
+ void noteVibratorOff(int uid);
+
+ /**
* Tell the manager that the currently active vibration has completed its vibration, from
* the perspective of the Effect. However, the VibrationThread may still be continuing with
* cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased}
@@ -108,16 +81,10 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
void onVibrationThreadReleased();
}
- private final Object mLock = new Object();
- private final WorkSource mWorkSource;
private final PowerManager.WakeLock mWakeLock;
- private final IBatteryStats mBatteryStatsService;
- private final VibrationSettings mVibrationSettings;
- private final DeviceVibrationEffectAdapter mDeviceEffectAdapter;
- private final Vibration mVibration;
- private final VibratorManagerHooks mVibratorManagerHooks;
- private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
- private final StepQueue mStepQueue = new StepQueue();
+ private final VibrationThread.VibratorManagerHooks mVibratorManagerHooks;
+
+ private final VibrationStepConductor mStepConductor;
private volatile boolean mStop;
private volatile boolean mForceStop;
@@ -127,30 +94,20 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
VibrationThread(Vibration vib, VibrationSettings vibrationSettings,
DeviceVibrationEffectAdapter effectAdapter,
SparseArray<VibratorController> availableVibrators, PowerManager.WakeLock wakeLock,
- IBatteryStats batteryStatsService, VibratorManagerHooks vibratorManagerHooks) {
- mVibration = vib;
- mVibrationSettings = vibrationSettings;
- mDeviceEffectAdapter = effectAdapter;
+ VibratorManagerHooks vibratorManagerHooks) {
mVibratorManagerHooks = vibratorManagerHooks;
- mWorkSource = new WorkSource(mVibration.uid);
mWakeLock = wakeLock;
- mBatteryStatsService = batteryStatsService;
-
- 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));
- }
- }
+ mStepConductor = new VibrationStepConductor(vib, vibrationSettings, effectAdapter,
+ availableVibrators, vibratorManagerHooks);
}
Vibration getVibration() {
- return mVibration;
+ return mStepConductor.getVibration();
}
@VisibleForTesting
SparseArray<VibratorController> getVibrators() {
- return mVibrators;
+ return mStepConductor.getVibrators();
}
@Override
@@ -179,7 +136,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
/** Runs the VibrationThread ensuring that the wake lock is acquired and released. */
private void runWithWakeLock() {
- mWakeLock.setWorkSource(mWorkSource);
+ mWakeLock.setWorkSource(mStepConductor.getWorkSource());
mWakeLock.acquire();
try {
runWithWakeLockAndDeathLink();
@@ -193,8 +150,9 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
* Called from within runWithWakeLock.
*/
private void runWithWakeLockAndDeathLink() {
+ IBinder vibrationBinderToken = mStepConductor.getVibration().token;
try {
- mVibration.token.linkToDeath(this, 0);
+ vibrationBinderToken.linkToDeath(this, 0);
} catch (RemoteException e) {
Slog.e(TAG, "Error linking vibration to token death", e);
clientVibrationCompleteIfNotAlready(Vibration.Status.IGNORED_ERROR_TOKEN);
@@ -206,7 +164,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
playVibration();
} finally {
try {
- mVibration.token.unlinkToDeath(this, 0);
+ vibrationBinderToken.unlinkToDeath(this, 0);
} catch (NoSuchElementException e) {
Slog.wtf(TAG, "Failed to unlink token", e);
}
@@ -220,11 +178,11 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
return;
}
mStop = true;
- synchronized (mLock) {
+ synchronized (mStepConductor.mLock) {
if (DEBUG) {
Slog.d(TAG, "Vibration cancelled");
}
- mLock.notify();
+ mStepConductor.mLock.notify();
}
}
@@ -235,36 +193,22 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
return;
}
mStop = mForceStop = true;
- synchronized (mLock) {
+ synchronized (mStepConductor.mLock) {
if (DEBUG) {
Slog.d(TAG, "Vibration cancelled immediately");
}
- mLock.notify();
+ mStepConductor.mLock.notify();
}
}
/** Notify current vibration that a synced step has completed. */
public void syncedVibrationComplete() {
- synchronized (mLock) {
- if (DEBUG) {
- Slog.d(TAG, "Synced vibration complete reported by vibrator manager");
- }
- for (int i = 0; i < mVibrators.size(); i++) {
- mStepQueue.notifyVibratorComplete(mVibrators.keyAt(i));
- }
- mLock.notify();
- }
+ mStepConductor.notifySyncedVibrationComplete();
}
/** Notify current vibration that a step has completed on given vibrator. */
public void vibratorComplete(int vibratorId) {
- synchronized (mLock) {
- if (DEBUG) {
- Slog.d(TAG, "Vibration complete reported by vibrator " + vibratorId);
- }
- mStepQueue.notifyVibratorComplete(vibratorId);
- mLock.notify();
- }
+ mStepConductor.notifyVibratorComplete(vibratorId);
}
// Indicate that the vibration is complete. This can be called multiple times only for
@@ -273,24 +217,26 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
private void clientVibrationCompleteIfNotAlready(Vibration.Status completedStatus) {
if (!mCalledVibrationCompleteCallback) {
mCalledVibrationCompleteCallback = true;
- mVibratorManagerHooks.onVibrationCompleted(mVibration.id, completedStatus);
+ mVibratorManagerHooks.onVibrationCompleted(
+ mStepConductor.getVibration().id, completedStatus);
}
}
private void playVibration() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
try {
- CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffect());
+ CombinedVibration.Sequential sequentialEffect =
+ toSequential(mStepConductor.getVibration().getEffect());
final int sequentialEffectSize = sequentialEffect.getEffects().size();
- mStepQueue.initializeForEffect(sequentialEffect);
+ mStepConductor.initializeForEffect(sequentialEffect);
- while (!mStepQueue.isFinished()) {
+ while (!mStepConductor.isFinished()) {
long waitMillisBeforeNextStep;
- synchronized (mLock) {
- waitMillisBeforeNextStep = mStepQueue.getWaitMillisBeforeNextStep();
+ synchronized (mStepConductor.mLock) {
+ waitMillisBeforeNextStep = mStepConductor.getWaitMillisBeforeNextStepLocked();
if (waitMillisBeforeNextStep > 0) {
try {
- mLock.wait(waitMillisBeforeNextStep);
+ mStepConductor.mLock.wait(waitMillisBeforeNextStep);
} catch (InterruptedException e) {
}
}
@@ -304,21 +250,21 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
}
// Run the step without holding the main lock, to avoid HAL interactions from
// blocking the thread.
- mStepQueue.runNextStep();
+ mStepConductor.runNextStep();
}
Vibration.Status status = mStop ? Vibration.Status.CANCELLED
- : mStepQueue.calculateVibrationStatus(sequentialEffectSize);
+ : mStepConductor.calculateVibrationStatus(sequentialEffectSize);
if (status != Vibration.Status.RUNNING && !mCalledVibrationCompleteCallback) {
// First time vibration stopped running, start clean-up tasks and notify
// callback immediately.
clientVibrationCompleteIfNotAlready(status);
if (status == Vibration.Status.CANCELLED) {
- mStepQueue.cancel();
+ mStepConductor.cancel();
}
}
if (mForceStop) {
// Cancel every step and stop playing them right away, even clean-up steps.
- mStepQueue.cancelImmediately();
+ mStepConductor.cancelImmediately();
clientVibrationCompleteIfNotAlready(Vibration.Status.CANCELLED);
break;
}
@@ -328,61 +274,6 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
}
}
- private void noteVibratorOn(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(mVibration.uid, duration);
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
- mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON,
- duration);
- } catch (RemoteException e) {
- }
- }
-
- private void noteVibratorOff() {
- try {
- mBatteryStatsService.noteVibratorOff(mVibration.uid);
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
- mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF,
- /* duration= */ 0);
- } catch (RemoteException e) {
- }
- }
-
- @Nullable
- private SingleVibratorStep nextVibrateStep(long startTime, VibratorController controller,
- VibrationEffect.Composed effect, int segmentIndex, long vibratorOffTimeout) {
- 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 EffectCompleteStep(startTime, /* cancelled= */ false, controller,
- vibratorOffTimeout);
- }
-
- VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
- if (segment instanceof PrebakedSegment) {
- return new PerformStep(startTime, controller, effect, segmentIndex, vibratorOffTimeout);
- }
- if (segment instanceof PrimitiveSegment) {
- return new ComposePrimitivesStep(startTime, controller, effect, segmentIndex,
- vibratorOffTimeout);
- }
- if (segment instanceof RampSegment) {
- return new ComposePwleStep(startTime, controller, effect, segmentIndex,
- vibratorOffTimeout);
- }
- return new AmplitudeStep(startTime, controller, effect, segmentIndex, vibratorOffTimeout);
- }
-
private static CombinedVibration.Sequential toSequential(CombinedVibration effect) {
if (effect instanceof CombinedVibration.Sequential) {
return (CombinedVibration.Sequential) effect;
@@ -392,1268 +283,4 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
.combine();
}
- /** Queue for {@link Step Steps}, sorted by their start time. */
- private final class StepQueue {
- @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;
-
- public void initializeForEffect(@NonNull CombinedVibration.Sequential vibration) {
- synchronized (mLock) {
- mPendingVibrateSteps++;
- mNextSteps.offer(new StartVibrateStep(vibration));
- }
- }
-
- 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 StartVibrateStep} 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("VibrationThread.this.mLock")
- public long getWaitMillisBeforeNextStep() {
- 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 StartVibrateStep) {
- 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) {
- processVibratorCompleteCallbacks();
- }
- }
- }
-
- /**
- * 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")
- public void notifyVibratorComplete(int vibratorId) {
- mCompletionNotifiedVibrators.offer(vibratorId);
- if (!mWaitToProcessVibratorCompleteCallbacks) {
- // No step is being played or cancelled now, process the callback right away.
- processVibratorCompleteCallbacks();
- }
- }
-
- /**
- * 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) {
- processVibratorCompleteCallbacks();
- }
- }
- }
-
- /**
- * 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) {
- processVibratorCompleteCallbacks();
- }
- }
- }
-
- @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 processVibratorCompleteCallbacks() {
- 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;
- }
- }
- }
- }
- }
-
- /**
- * 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.
- */
- private abstract class Step implements Comparable<Step> {
- public final long startTime;
-
- Step(long startTime) {
- this.startTime = startTime;
- }
-
- /**
- * 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);
- }
- }
-
- /**
- * Starts a sync vibration.
- *
- * <p>If this step has successfully started playing a vibration on any vibrator, it will always
- * add a {@link FinishVibrateStep} 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 StartVibrateStep} if the
- * sequential effect isn't finished yet.
- */
- private final class StartVibrateStep extends Step {
- public final CombinedVibration.Sequential sequentialEffect;
- public final int currentIndex;
-
- private long mVibratorsOnMaxDuration;
-
- StartVibrateStep(CombinedVibration.Sequential effect) {
- this(SystemClock.uptimeMillis() + effect.getDelays().get(0), effect, /* index= */ 0);
- }
-
- StartVibrateStep(long startTime, CombinedVibration.Sequential effect, int index) {
- super(startTime);
- sequentialEffect = effect;
- currentIndex = index;
- }
-
- @Override
- public long getVibratorOnDuration() {
- return mVibratorsOnMaxDuration;
- }
-
- @Override
- public List<Step> play() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartVibrateStep");
- List<Step> nextSteps = new ArrayList<>();
- mVibratorsOnMaxDuration = -1;
- try {
- if (DEBUG) {
- Slog.d(TAG, "StartVibrateStep 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);
- noteVibratorOn(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 FinishVibrateStep(this) : nextStep();
- if (nextStep != null) {
- nextSteps.add(nextStep);
- }
- }
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- return nextSteps;
- }
-
- @Override
- public List<Step> cancel() {
- return EMPTY_STEP_LIST;
- }
-
- @Override
- public void cancelImmediately() {
- }
-
- /**
- * Create the next {@link StartVibrateStep} to play this sequential effect, starting at the
- * time this method is called, or null if sequence is complete.
- */
- @Nullable
- private 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 StartVibrateStep(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;
- }
-
- SingleVibratorStep[] steps = new SingleVibratorStep[vibratorCount];
- long vibrationStartTime = SystemClock.uptimeMillis();
- for (int i = 0; i < vibratorCount; i++) {
- steps[i] = nextVibrateStep(vibrationStartTime,
- mVibrators.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.
- synchronized (mLock) {
- boolean hasPrepared = false;
- boolean hasTriggered = false;
- long maxDuration = 0;
- try {
- hasPrepared = mVibratorManagerHooks.prepareSyncedVibration(
- effectMapping.getRequiredSyncCapabilities(),
- effectMapping.getVibratorIds());
-
- 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 && maxDuration > 0) {
- hasTriggered = mVibratorManagerHooks.triggerSyncedVibration(mVibration.id);
- }
- return maxDuration;
- } finally {
- if (hasPrepared && !hasTriggered) {
- // Trigger has failed or all steps were ignored by the vibrators.
- mVibratorManagerHooks.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(SingleVibratorStep 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());
- }
- }
-
- /**
- * Finish a sync vibration started by a {@link StartVibrateStep}.
- *
- * <p>This only plays after all active vibrators steps have finished, and adds a {@link
- * StartVibrateStep} to the queue if the sequential effect isn't finished yet.
- */
- private final class FinishVibrateStep extends Step {
- public final StartVibrateStep startedStep;
-
- FinishVibrateStep(StartVibrateStep startedStep) {
- super(Long.MAX_VALUE); // No predefined startTime, just wait for all steps in the queue.
- 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, "FinishVibrateStep");
- try {
- if (DEBUG) {
- Slog.d(TAG, "FinishVibrateStep for effect #" + startedStep.currentIndex);
- }
- noteVibratorOff();
- Step nextStep = startedStep.nextStep();
- return nextStep == null ? EMPTY_STEP_LIST : Arrays.asList(nextStep);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
-
- @Override
- public List<Step> cancel() {
- cancelImmediately();
- return EMPTY_STEP_LIST;
- }
-
- @Override
- public void cancelImmediately() {
- noteVibratorOff();
- }
- }
-
- /**
- * Represent a step on a single vibrator that plays one or more segments from a
- * {@link VibrationEffect.Composed} effect.
- */
- private abstract class SingleVibratorStep extends Step {
- public final VibratorController controller;
- public final VibrationEffect.Composed effect;
- public final int segmentIndex;
- public final long vibratorOffTimeout;
-
- long mVibratorOnResult;
- boolean mVibratorCompleteCallbackReceived;
-
- /**
- * @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
- * triggered when the completion callback is received, 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;
- }
-
- @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 && (vibratorOffTimeout > SystemClock.uptimeMillis());
- }
-
- @Override
- public List<Step> cancel() {
- return Arrays.asList(new EffectCompleteStep(SystemClock.uptimeMillis(),
- /* cancelled= */ true, controller, vibratorOffTimeout));
- }
-
- @Override
- public void cancelImmediately() {
- 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();
- }
-
- void changeAmplitude(float amplitude) {
- if (DEBUG) {
- Slog.d(TAG, "Amplitude changed on vibrator " + controller.getVibratorInfo().getId()
- + " to " + amplitude);
- }
- controller.setAmplitude(amplitude);
- }
-
- /** 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 #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.
- */
- 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 nextStartTime = SystemClock.uptimeMillis() + mVibratorOnResult;
- long nextVibratorOffTimeout = nextStartTime + CALLBACKS_EXTRA_TIMEOUT;
- return nextSteps(nextStartTime, 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) {
- // 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(Math.max(startTime, vibratorOffTimeout), controller, effect, index,
- vibratorOffTimeout);
- }
-
- @Override
- public List<Step> play() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformStep");
- try {
- VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
- if (!(segment instanceof PrebakedSegment)) {
- Slog.w(TAG, "Ignoring wrong segment for a PerformStep: " + segment);
- return skipToNextSteps(/* segmentsSkipped= */ 1);
- }
-
- 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.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);
- }
- }
-
- /**
- * 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) {
- // 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(Math.max(startTime, vibratorOffTimeout), controller, effect, index,
- vibratorOffTimeout);
- }
-
- @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(TAG, "Ignoring wrong segment for a ComposePrimitivesStep: "
- + effect.getSegments().get(segmentIndex));
- return skipToNextSteps(/* segmentsSkipped= */ 1);
- }
-
- if (DEBUG) {
- Slog.d(TAG, "Compose " + primitives + " primitives on vibrator "
- + controller.getVibratorInfo().getId());
- }
- mVibratorOnResult = controller.on(
- primitives.toArray(new PrimitiveSegment[primitives.size()]),
- mVibration.id);
-
- return nextSteps(/* segmentsPlayed= */ 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) {
- // 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(Math.max(startTime, vibratorOffTimeout), controller, effect, index,
- vibratorOffTimeout);
- }
-
- @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(TAG, "Ignoring wrong segment for a ComposePwleStep: "
- + effect.getSegments().get(segmentIndex));
- return skipToNextSteps(/* segmentsSkipped= */ 1);
- }
-
- if (DEBUG) {
- Slog.d(TAG, "Compose " + pwles + " 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);
- }
- }
- }
-
- /**
- * 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.
- */
- private final class EffectCompleteStep extends SingleVibratorStep {
- private final boolean mCancelled;
-
- EffectCompleteStep(long startTime, boolean cancelled, VibratorController controller,
- long vibratorOffTimeout) {
- super(startTime, controller, /* effect= */ null, /* index= */ -1, vibratorOffTimeout);
- 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 OffStep(SystemClock.uptimeMillis(), controller));
- }
- return super.cancel();
- }
-
- @Override
- public List<Step> play() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "EffectCompleteStep");
- try {
- if (DEBUG) {
- Slog.d(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 EMPTY_STEP_LIST;
- }
-
- float currentAmplitude = controller.getCurrentAmplitude();
- long remainingOnDuration =
- vibratorOffTimeout - CALLBACKS_EXTRA_TIMEOUT - SystemClock.uptimeMillis();
- long rampDownDuration =
- Math.min(remainingOnDuration, mVibrationSettings.getRampDownDuration());
- long stepDownDuration = mVibrationSettings.getRampStepDuration();
- if (currentAmplitude < 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 EMPTY_STEP_LIST;
- } else {
- return Arrays.asList(new OffStep(vibratorOffTimeout, controller));
- }
- }
-
- if (DEBUG) {
- Slog.d(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 : vibratorOffTimeout;
- return Arrays.asList(new RampOffStep(startTime, amplitudeTarget, amplitudeDelta,
- controller, newVibratorOffTimeout));
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
- }
-
- /** Represents a step to ramp down the vibrator amplitude before turning it off. */
- private final class RampOffStep extends SingleVibratorStep {
- private final float mAmplitudeTarget;
- private final float mAmplitudeDelta;
-
- RampOffStep(long startTime, float amplitudeTarget, float amplitudeDelta,
- VibratorController controller, long vibratorOffTimeout) {
- super(startTime, controller, /* effect= */ null, /* index= */ -1, vibratorOffTimeout);
- mAmplitudeTarget = amplitudeTarget;
- mAmplitudeDelta = amplitudeDelta;
- }
-
- @Override
- public boolean isCleanUp() {
- return true;
- }
-
- @Override
- public List<Step> cancel() {
- return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller));
- }
-
- @Override
- public List<Step> play() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffStep");
- try {
- if (DEBUG) {
- long latency = SystemClock.uptimeMillis() - startTime;
- Slog.d(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 EMPTY_STEP_LIST;
- }
-
- changeAmplitude(mAmplitudeTarget);
-
- float newAmplitudeTarget = mAmplitudeTarget - mAmplitudeDelta;
- if (newAmplitudeTarget < RAMP_OFF_AMPLITUDE_MIN) {
- // Vibrator amplitude cannot go further down, just turn it off.
- return Arrays.asList(new OffStep(vibratorOffTimeout, controller));
- }
- return Arrays.asList(new RampOffStep(
- startTime + mVibrationSettings.getRampStepDuration(), newAmplitudeTarget,
- mAmplitudeDelta, controller, vibratorOffTimeout));
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
- }
-
- /**
- * 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.
- */
- private final class OffStep extends SingleVibratorStep {
-
- OffStep(long startTime, VibratorController controller) {
- super(startTime, controller, /* effect= */ null, /* index= */ -1, startTime);
- }
-
- @Override
- public boolean isCleanUp() {
- return true;
- }
-
- @Override
- public List<Step> cancel() {
- return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller));
- }
-
- @Override
- public void cancelImmediately() {
- stopVibrating();
- }
-
- @Override
- public List<Step> play() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "OffStep");
- try {
- stopVibrating();
- return EMPTY_STEP_LIST;
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
- }
-
- /**
- * 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;
-
- AmplitudeStep(long startTime, VibratorController controller,
- VibrationEffect.Composed effect, int index, long vibratorOffTimeout) {
- // This step has a fixed startTime coming from the timings of the waveform it's playing.
- super(startTime, controller, effect, index, vibratorOffTimeout);
- mNextOffTime = vibratorOffTimeout;
- }
-
- @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() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "AmplitudeStep");
- try {
- long now = SystemClock.uptimeMillis();
- long latency = now - startTime;
- if (DEBUG) {
- Slog.d(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 AmplitudeStep(startTime, controller, effect,
- segmentIndex, mNextOffTime));
- }
-
- VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
- if (!(segment instanceof StepSegment)) {
- 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 skipToNextSteps(/* segmentsSkipped= */ 1);
- }
-
- float amplitude = stepSegment.getAmplitude();
- if (amplitude == 0) {
- if (vibratorOffTimeout > 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 + 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 vibratorOffTimeout;
- }
- 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 + CALLBACKS_EXTRA_TIMEOUT;
- }
-
- private long startVibrating(long duration) {
- if (DEBUG) {
- Slog.d(TAG, "Turning on vibrator " + controller.getVibratorInfo().getId() + " for "
- + duration + "ms");
- }
- return controller.on(duration, mVibration.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 += mVibrationSettings.getRampDownDuration();
- }
- return timing;
- }
- }
-
- /**
- * 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.
- */
- private final class DeviceEffectMap {
- private final SparseArray<VibrationEffect.Composed> mVibratorEffects;
- private final int[] mVibratorIds;
- private final long mRequiredSyncCapabilities;
-
- DeviceEffectMap(CombinedVibration.Mono mono) {
- mVibratorEffects = new SparseArray<>(mVibrators.size());
- mVibratorIds = new int[mVibrators.size()];
- for (int i = 0; i < mVibrators.size(); i++) {
- int vibratorId = mVibrators.keyAt(i);
- VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo();
- VibrationEffect effect = mDeviceEffectAdapter.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<VibrationEffect> stereoEffects = stereo.getEffects();
- mVibratorEffects = new SparseArray<>();
- for (int i = 0; i < stereoEffects.size(); i++) {
- int vibratorId = stereoEffects.keyAt(i);
- if (mVibrators.contains(vibratorId)) {
- VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo();
- VibrationEffect effect = mDeviceEffectAdapter.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;
- }
- }
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 63f3af3f3095..01f9d0b94879 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -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);
@@ -1141,6 +1150,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
}
@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) {
Slog.d(TAG, "Vibration " + vibrationId + " finished with status " + status);
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index e1a4989e5a05..01e306e744fb 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -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;
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index ccdb1050d32c..19111e5d16e9 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -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);
@@ -222,6 +228,11 @@ public class VibratorManagerServiceTest {
}
@Override
+ IBatteryStats getBatteryStatsService() {
+ return mBatteryStatsMock;
+ }
+
+ @Override
VibratorController createVibratorController(int vibratorId,
VibratorController.OnVibrationCompleteListener listener) {
return mVibratorProviders.get(vibratorId)
@@ -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