diff options
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 |