From 4e125b212a97ed5281f3a798f955ff4e12fa546c Mon Sep 17 00:00:00 2001 From: Yeabkal Wubshit Date: Wed, 2 Nov 2022 22:07:21 -0700 Subject: [PATCH] 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 --- core/api/test-current.txt | 2 + core/java/android/os/VibrationEffect.java | 95 +++++ .../src/android/os/VibrationEffectTest.java | 362 +++++++++++++++++- 3 files changed, 456 insertions(+), 3 deletions(-) diff --git a/core/api/test-current.txt b/core/api/test-current.txt index fa7317849b311..725cef6d13445 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -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 getSegments(); diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index 71bc4b36f4e02..3448a9eed80c5 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -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. + * + *

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}. + * + *

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 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; + } } /** diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java index f7ca822c36e2a..0c7ff4a762b51 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -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,