Create API to Compute Vibration Pattern Equivalent to a VibrationEffect

This is initially thought of in
go/vibration-effects-for-notifications-dd, and can be used whenever a
vibration pattern is needed to be extracted from a VibrationEffect. See
also: go/vibration-changes-for-notification-channels

Bug: 241732519
Test: unit tests
Change-Id: Ieccbe95d40e638dec23014cc18ba11c0b5754fd8
This commit is contained in:
Yeabkal Wubshit
2022-11-02 22:07:21 -07:00
parent 79ffdd3932
commit 4e125b212a
3 changed files with 456 additions and 3 deletions

View File

@@ -1908,6 +1908,7 @@ package android.os {
}
public abstract class VibrationEffect implements android.os.Parcelable {
method @Nullable public abstract long[] computeCreateWaveformOffOnTimingsOrNull();
method public static android.os.VibrationEffect get(int);
method public static android.os.VibrationEffect get(int, boolean);
method @Nullable public static android.os.VibrationEffect get(android.net.Uri, android.content.Context);
@@ -1922,6 +1923,7 @@ package android.os {
}
public static final class VibrationEffect.Composed extends android.os.VibrationEffect {
method @Nullable public long[] computeCreateWaveformOffOnTimingsOrNull();
method public long getDuration();
method public int getRepeatIndex();
method @NonNull public java.util.List<android.os.vibrator.VibrationEffectSegment> getSegments();

View File

@@ -226,6 +226,31 @@ public abstract class VibrationEffect implements Parcelable {
return createWaveform(timings, amplitudes, repeat);
}
/**
* Computes a legacy vibration pattern (i.e. a pattern with duration values for "off/on"
* vibration components) that is equivalent to this VibrationEffect.
*
* <p>All non-repeating effects created with {@link #createWaveform(int[], int)} are convertible
* into an equivalent vibration pattern with this method. It is not guaranteed that an effect
* created with other means becomes converted into an equivalent legacy vibration pattern, even
* if it has an equivalent vibration pattern. If this method is unable to create an equivalent
* vibration pattern for such effects, it will return {@code null}.
*
* <p>Note that a valid equivalent long[] pattern cannot be created for an effect that has any
* form of repeating behavior, regardless of how the effect was created. For repeating effects,
* the method will always return {@code null}.
*
* @return a long array representing a vibration pattern equivalent to the VibrationEffect, if
* the method successfully derived a vibration pattern equivalent to the effect
* (this will always be the case if the effect was created via
* {@link #createWaveform(int[], int)} and is non-repeating). Otherwise, returns
* {@code null}.
* @hide
*/
@TestApi
@Nullable
public abstract long[] computeCreateWaveformOffOnTimingsOrNull();
/**
* Create a waveform vibration.
*
@@ -641,6 +666,51 @@ public abstract class VibrationEffect implements Parcelable {
return mRepeatIndex;
}
/** @hide */
@Override
@Nullable
public long[] computeCreateWaveformOffOnTimingsOrNull() {
if (getRepeatIndex() >= 0) {
// Repeating effects cannot be fully represented as a long[] legacy pattern.
return null;
}
List<VibrationEffectSegment> segments = getSegments();
// The maximum possible size of the final pattern is 1 plus the number of segments in
// the original effect. This is because we will add an empty "off" segment at the
// start of the pattern if the first segment of the original effect is an "on" segment.
// (because the legacy patterns start with an "off" pattern). Other than this one case,
// we will add the durations of back-to-back segments of similar amplitudes (amplitudes
// that are all "on" or "off") and create a pattern entry for the total duration, which
// will not take more number pattern entries than the number of segments processed.
long[] patternBuffer = new long[segments.size() + 1];
int patternIndex = 0;
for (int i = 0; i < segments.size(); i++) {
StepSegment stepSegment =
castToValidStepSegmentForOffOnTimingsOrNull(segments.get(i));
if (stepSegment == null) {
// This means that there is 1 or more segments of this effect that is/are not a
// possible component of a legacy vibration pattern. Thus, the VibrationEffect
// does not have any equivalent legacy vibration pattern.
return null;
}
boolean isSegmentOff = stepSegment.getAmplitude() == 0;
// Even pattern indices are "off", and odd pattern indices are "on"
boolean isCurrentPatternIndexOff = (patternIndex % 2) == 0;
if (isSegmentOff != isCurrentPatternIndexOff) {
// Move the pattern index one step ahead, so that the current segment's
// "off"/"on" property matches that of the index's
++patternIndex;
}
patternBuffer[patternIndex] += stepSegment.getDuration();
}
return Arrays.copyOf(patternBuffer, patternIndex + 1);
}
/** @hide */
@Override
public void validate() {
@@ -806,6 +876,31 @@ public abstract class VibrationEffect implements Parcelable {
return new Composed[size];
}
};
/**
* Casts a provided {@link VibrationEffectSegment} to a {@link StepSegment} and returns it,
* only if it can possibly be a segment for an effect created via
* {@link #createWaveform(int[], int)}. Otherwise, returns {@code null}.
*/
@Nullable
private static StepSegment castToValidStepSegmentForOffOnTimingsOrNull(
VibrationEffectSegment segment) {
if (!(segment instanceof StepSegment)) {
return null;
}
StepSegment stepSegment = (StepSegment) segment;
if (stepSegment.getFrequencyHz() != 0) {
return null;
}
float amplitude = stepSegment.getAmplitude();
if (amplitude != 0 && amplitude != DEFAULT_AMPLITUDE) {
return null;
}
return stepSegment;
}
}
/**

View File

@@ -16,6 +16,7 @@
package android.os;
import static android.os.VibrationEffect.DEFAULT_AMPLITUDE;
import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
@@ -48,6 +49,7 @@ import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.Duration;
import java.util.Arrays;
@Presubmit
@RunWith(MockitoJUnitRunner.class)
@@ -62,15 +64,362 @@ public class VibrationEffectTest {
private static final int TEST_AMPLITUDE = 100;
private static final long[] TEST_TIMINGS = new long[] { 100, 100, 200 };
private static final int[] TEST_AMPLITUDES =
new int[] { 255, 0, VibrationEffect.DEFAULT_AMPLITUDE };
new int[] { 255, 0, DEFAULT_AMPLITUDE };
private static final VibrationEffect TEST_ONE_SHOT =
VibrationEffect.createOneShot(TEST_TIMING, TEST_AMPLITUDE);
private static final VibrationEffect DEFAULT_ONE_SHOT =
VibrationEffect.createOneShot(TEST_TIMING, VibrationEffect.DEFAULT_AMPLITUDE);
VibrationEffect.createOneShot(TEST_TIMING, DEFAULT_AMPLITUDE);
private static final VibrationEffect TEST_WAVEFORM =
VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1);
@Test
public void computeLegacyPattern_timingsAndAmplitudes_zeroAmplitudesOnEvenIndices() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3, 4, 5},
/* amplitudes= */ new int[] {0, DEFAULT_AMPLITUDE, 0, DEFAULT_AMPLITUDE, 0},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {1, 2, 3, 4, 5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_zeroAmplitudesOnOddIndices() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3, 4, 5},
/* amplitudes= */ new int[] {
DEFAULT_AMPLITUDE, 0, DEFAULT_AMPLITUDE, 0, DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {0, 1, 2, 3, 4, 5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_zeroAmplitudesAtTheStart() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3},
/* amplitudes= */ new int[] {0, 0, DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {3, 3};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_zeroAmplitudesAtTheEnd() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3},
/* amplitudes= */ new int[] {DEFAULT_AMPLITUDE, 0, 0},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {0, 1, 5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_allDefaultAmplitudes() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3},
/* amplitudes= */ new int[] {
DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {0, 6};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_allZeroAmplitudes() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3},
/* amplitudes= */ new int[] {0, 0, 0},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {6};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_sparsedZeroAmplitudes() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3, 4, 5, 6, 7},
/* amplitudes= */ new int[] {
0, 0, DEFAULT_AMPLITUDE, 0, DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE, 0},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {3, 3, 4, 11, 7};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_oneTimingWithDefaultAmplitude() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1},
/* amplitudes= */ new int[] {DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {0, 1};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_oneTimingWithZeroAmplitude() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1},
/* amplitudes= */ new int[] {0},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {1};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_repeating() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3, 4, 5},
/* amplitudes= */ new int[] {0, DEFAULT_AMPLITUDE, 0, DEFAULT_AMPLITUDE, 0},
/* repeatIndex= */ 0);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3, 4, 5},
/* amplitudes= */ new int[] {0, DEFAULT_AMPLITUDE, 0, DEFAULT_AMPLITUDE, 0},
/* repeatIndex= */ 3);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2},
/* amplitudes= */ new int[] {DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE},
/* repeatIndex= */ 1);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsAndAmplitudes_badAmplitude() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1},
/* amplitudes= */ new int[] {200},
/* repeatIndex= */ -1);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsOnly_nonZeroTimings() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {1, 2, 3};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsOnly_oneValue() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {5},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsOnly_zeroesAtTheEnd() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3, 0, 0},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {1, 2, 3, 0, 0};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsOnly_zeroesAtTheStart() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {0, 0, 1, 2, 3},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {0, 0, 1, 2, 3};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsOnly_zeroesAtTheMiddle() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 0, 0, 3, 4, 5},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {1, 2, 0, 0, 3, 4, 5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsOnly_sparsedZeroes() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {0, 1, 2, 0, 0, 3, 4, 5, 0},
/* repeatIndex= */ -1);
long[] expectedPattern = new long[] {0, 1, 2, 0, 0, 3, 4, 5, 0};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_timingsOnly_repeating() {
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {0, 1, 2, 0, 0, 3, 4, 5, 0},
/* repeatIndex= */ 0);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
effect = VibrationEffect.createWaveform(
/* timings= */ new long[] {1, 2, 3, 4},
/* repeatIndex= */ 2);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_notPatternPased() {
VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_oneShot_defaultAmplitude() {
VibrationEffect effect = VibrationEffect.createOneShot(
/* milliseconds= */ 5, /* ampliutde= */ DEFAULT_AMPLITUDE);
long[] expectedPattern = new long[] {0, 5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_oneShot_badAmplitude() {
VibrationEffect effect = VibrationEffect.createOneShot(
/* milliseconds= */ 5, /* ampliutde= */ 50);
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_composition_noOffDuration() {
VibrationEffect effect = VibrationEffect.startComposition()
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {5},
/* repeatIndex= */ -1))
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {2, 3},
/* repeatIndex= */ -1))
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {10, 20},
/* amplitudes= */ new int[] {DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1))
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {4, 5},
/* amplitudes= */ new int[] {0, DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1))
.compose();
long[] expectedPattern = new long[] {7, 33, 4, 5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_composition_withOffDuration() {
VibrationEffect effect = VibrationEffect.startComposition()
.addOffDuration(Duration.ofMillis(20))
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {10, 20},
/* amplitudes= */ new int[] {0, DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1))
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {30, 40},
/* amplitudes= */ new int[] {DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE},
/* repeatIndex= */ -1))
.addOffDuration(Duration.ofMillis(10))
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {4, 5},
/* repeatIndex= */ -1))
.addOffDuration(Duration.ofMillis(5))
.compose();
long[] expectedPattern = new long[] {30, 90, 14, 5, 5};
assertArrayEq(expectedPattern, effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_composition_withPrimitives() {
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
.addOffDuration(Duration.ofMillis(20))
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {5},
/* repeatIndex= */ -1))
.compose();
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_composition_repeating() {
VibrationEffect effect = VibrationEffect.startComposition()
.addEffect(
VibrationEffect.createWaveform(
/* timings= */ new long[] {5},
/* repeatIndex= */ -1))
.repeatEffectIndefinitely(
VibrationEffect.createWaveform(
/* timings= */ new long[] {2, 3},
/* repeatIndex= */ -1))
.compose();
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void computeLegacyPattern_effectsViaStartWaveform() {
// Effects created via startWaveform are not expected to be converted to long[] patterns, as
// they are not configured to always play with the default amplitude.
VibrationEffect effect = VibrationEffect.startWaveform(targetFrequency(60))
.addTransition(Duration.ofMillis(100), targetAmplitude(1), targetFrequency(120))
.addSustain(Duration.ofMillis(200))
.addTransition(Duration.ofMillis(100), targetAmplitude(0), targetFrequency(60))
.build();
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
effect = VibrationEffect.startWaveform(targetFrequency(60))
.addTransition(Duration.ofMillis(80), targetAmplitude(1))
.addSustain(Duration.ofMillis(200))
.addTransition(Duration.ofMillis(100), targetAmplitude(0))
.build();
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
effect = VibrationEffect.startWaveform(targetFrequency(60))
.addTransition(Duration.ofMillis(100), targetFrequency(50))
.addSustain(Duration.ofMillis(50))
.addTransition(Duration.ofMillis(20), targetFrequency(75))
.build();
assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
}
@Test
public void getRingtones_noPrebakedRingtones() {
Resources r = mockRingtoneResources(new String[0]);
@@ -100,7 +449,7 @@ public class VibrationEffectTest {
@Test
public void testValidateOneShot() {
VibrationEffect.createOneShot(1, 255).validate();
VibrationEffect.createOneShot(1, VibrationEffect.DEFAULT_AMPLITUDE).validate();
VibrationEffect.createOneShot(1, DEFAULT_AMPLITUDE).validate();
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.createOneShot(-1, 255).validate());
@@ -501,6 +850,13 @@ public class VibrationEffectTest {
assertTrue(VibrationEffect.get(VibrationEffect.EFFECT_TICK).isHapticFeedbackCandidate());
}
private void assertArrayEq(long[] expected, long[] actual) {
assertTrue(
String.format("Expected pattern %s, but was %s",
Arrays.toString(expected), Arrays.toString(actual)),
Arrays.equals(expected, actual));
}
private Resources mockRingtoneResources() {
return mockRingtoneResources(new String[]{
RINGTONE_URI_1,