blob: d5cd344d93ce3e669f055c005df517812b6e6b07 [file] [log] [blame]
/*
* Copyright (C) 2021 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.VibratorInfo;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Adapter that applies the ramp down duration config to bring down the vibrator amplitude smoothly.
*
* <p>This prevents the device from ringing when it cannot handle abrupt changes between ON and OFF
* states. This will not change other types of abrupt amplitude changes in the original effect. The
* effect overall duration is preserved by this transformation.
*
* <p>Waveforms with ON/OFF segments are handled gracefully by the ramp down changes. Each OFF
* segment preceded by an ON segment will be shortened, and a ramp or step down will be added to the
* transition between ON and OFF. The ramps/steps can be shorter than the configured duration in
* order to preserve the waveform timings, but they will still soften the ringing effect.
*
* <p>If the segment preceding an OFF segment a {@link RampSegment} then a new ramp segment will be
* added to bring the amplitude down. If it is a {@link StepSegment} then a sequence of steps will
* be used to bring the amplitude down to zero. This ensures that the transition from the last
* amplitude to zero will be handled by the same vibrate method.
*/
final class RampDownAdapter implements VibrationEffectAdapters.SegmentsAdapter<VibratorInfo> {
private final int mRampDownDuration;
private final int mStepDuration;
RampDownAdapter(int rampDownDuration, int stepDuration) {
mRampDownDuration = rampDownDuration;
mStepDuration = stepDuration;
}
@Override
public int apply(List<VibrationEffectSegment> segments, int repeatIndex,
VibratorInfo info) {
if (mRampDownDuration <= 0) {
// Nothing to do, no ramp down duration configured.
return repeatIndex;
}
repeatIndex = addRampDownToZeroAmplitudeSegments(segments, repeatIndex);
repeatIndex = addRampDownToLoop(segments, repeatIndex);
return repeatIndex;
}
/**
* This will add ramp or steps down to zero as follows:
*
* <ol>
* <li>Remove the OFF segment that follows a segment of non-zero amplitude;
* <li>Add a single {@link RampSegment} or a list of {@link StepSegment} starting at the
* previous segment's amplitude and frequency, with min between the configured ramp down
* duration or the removed segment's duration;
* <li>Add a zero amplitude segment following the steps, if necessary, to fill the remaining
* duration;
* </ol>
*/
private int addRampDownToZeroAmplitudeSegments(List<VibrationEffectSegment> segments,
int repeatIndex) {
int newRepeatIndex = repeatIndex;
int newSegmentCount = segments.size();
for (int i = 1; i < newSegmentCount; i++) {
VibrationEffectSegment previousSegment = segments.get(i - 1);
if (!isOffSegment(segments.get(i))
|| !endsWithNonZeroAmplitude(previousSegment)) {
continue;
}
List<VibrationEffectSegment> replacementSegments = null;
long offDuration = segments.get(i).getDuration();
if (previousSegment instanceof StepSegment) {
float previousAmplitude = ((StepSegment) previousSegment).getAmplitude();
float previousFrequency = ((StepSegment) previousSegment).getFrequency();
replacementSegments =
createStepsDown(previousAmplitude, previousFrequency, offDuration);
} else if (previousSegment instanceof RampSegment) {
float previousAmplitude = ((RampSegment) previousSegment).getEndAmplitude();
float previousFrequency = ((RampSegment) previousSegment).getEndFrequency();
if (offDuration <= mRampDownDuration) {
// Replace the zero amplitude segment with a ramp down of same duration, to
// preserve waveform timings and still soften the transition to zero.
replacementSegments = Arrays.asList(
createRampDown(previousAmplitude, previousFrequency, offDuration));
} else {
// Replace the zero amplitude segment with a ramp down of configured duration
// followed by a shorter off segment.
replacementSegments = Arrays.asList(
createRampDown(previousAmplitude, previousFrequency, mRampDownDuration),
createRampDown(0, previousFrequency, offDuration - mRampDownDuration));
}
}
if (replacementSegments != null) {
int segmentsAdded = replacementSegments.size() - 1;
segments.remove(i);
segments.addAll(i, replacementSegments);
if (repeatIndex > i) {
newRepeatIndex += segmentsAdded;
}
i += segmentsAdded;
newSegmentCount += segmentsAdded;
}
}
return newRepeatIndex;
}
/**
* This will ramps down to zero at the repeating index of the given effect, if set, only if
* the last segment ends at a non-zero amplitude and the repeating segment has zero amplitude.
* The update is described as:
*
* <ol>
* <li>Add a ramp or sequence of steps down to zero following the last segment, with the min
* between the removed segment duration and the configured ramp down duration;
* <li>Skip the zero-amplitude segment by incrementing the repeat index, splitting it if
* necessary to skip the correct amount;
* </ol>
*/
private int addRampDownToLoop(List<VibrationEffectSegment> segments, int repeatIndex) {
if (repeatIndex < 0) {
// Nothing to do, no ramp down duration configured or effect is not repeating.
return repeatIndex;
}
int segmentCount = segments.size();
if (!endsWithNonZeroAmplitude(segments.get(segmentCount - 1))
|| !isOffSegment(segments.get(repeatIndex))) {
// Nothing to do, not going back from a positive amplitude to a off segment.
return repeatIndex;
}
VibrationEffectSegment lastSegment = segments.get(segmentCount - 1);
VibrationEffectSegment offSegment = segments.get(repeatIndex);
long offDuration = offSegment.getDuration();
if (offDuration > mRampDownDuration) {
// Split the zero amplitude segment and start repeating from the second half, to
// preserve waveform timings. This will update the waveform as follows:
// R R+1
// | ____ | ____
// _|__/ => __|_/ \
segments.set(repeatIndex, updateDuration(offSegment, offDuration - mRampDownDuration));
segments.add(repeatIndex, updateDuration(offSegment, mRampDownDuration));
}
// Skip the zero amplitude segment and append ramp/steps down at the end.
repeatIndex++;
if (lastSegment instanceof StepSegment) {
float previousAmplitude = ((StepSegment) lastSegment).getAmplitude();
float previousFrequency = ((StepSegment) lastSegment).getFrequency();
segments.addAll(createStepsDown(previousAmplitude, previousFrequency,
Math.min(offDuration, mRampDownDuration)));
} else if (lastSegment instanceof RampSegment) {
float previousAmplitude = ((RampSegment) lastSegment).getEndAmplitude();
float previousFrequency = ((RampSegment) lastSegment).getEndFrequency();
segments.add(createRampDown(previousAmplitude, previousFrequency,
Math.min(offDuration, mRampDownDuration)));
}
return repeatIndex;
}
private List<VibrationEffectSegment> createStepsDown(float amplitude, float frequency,
long duration) {
// Step down for at most the configured ramp duration.
int stepCount = (int) Math.min(duration, mRampDownDuration) / mStepDuration;
float amplitudeStep = amplitude / stepCount;
List<VibrationEffectSegment> steps = new ArrayList<>();
for (int i = 1; i < stepCount; i++) {
steps.add(new StepSegment(amplitude - i * amplitudeStep, frequency, mStepDuration));
}
int remainingDuration = (int) duration - mStepDuration * (stepCount - 1);
steps.add(new StepSegment(0, frequency, remainingDuration));
return steps;
}
private static RampSegment createRampDown(float amplitude, float frequency, long duration) {
return new RampSegment(amplitude, /* endAmplitude= */ 0, frequency, frequency,
(int) duration);
}
private static VibrationEffectSegment updateDuration(VibrationEffectSegment segment,
long newDuration) {
if (segment instanceof RampSegment) {
RampSegment ramp = (RampSegment) segment;
return new RampSegment(ramp.getStartAmplitude(), ramp.getEndAmplitude(),
ramp.getStartFrequency(), ramp.getEndFrequency(), (int) newDuration);
} else if (segment instanceof StepSegment) {
StepSegment step = (StepSegment) segment;
return new StepSegment(step.getAmplitude(), step.getFrequency(), (int) newDuration);
}
return segment;
}
/** Returns true if the segment is a ramp or a step that starts and ends at zero amplitude. */
private static boolean isOffSegment(VibrationEffectSegment segment) {
if (segment instanceof StepSegment) {
StepSegment ramp = (StepSegment) segment;
return ramp.getAmplitude() == 0;
} else if (segment instanceof RampSegment) {
RampSegment ramp = (RampSegment) segment;
return ramp.getStartAmplitude() == 0 && ramp.getEndAmplitude() == 0;
}
return false;
}
/** Returns true if the segment is a ramp or a step that ends at a non-zero amplitude. */
private static boolean endsWithNonZeroAmplitude(VibrationEffectSegment segment) {
if (segment instanceof StepSegment) {
return ((StepSegment) segment).getAmplitude() != 0;
} else if (segment instanceof RampSegment) {
return ((RampSegment) segment).getEndAmplitude() != 0;
}
return false;
}
}