Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4e125b21 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit
Browse files

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
parent 79ffdd39
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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();
+95 −0
Original line number Diff line number Diff line
@@ -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.
     *
@@ -643,6 +668,51 @@ public abstract class VibrationEffect implements Parcelable {

         /** @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() {
            int segmentCount = mSegments.size();
            boolean hasNonZeroDuration = false;
@@ -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;
        }
    }

    /**
+359 −3
Original line number Diff line number Diff line
@@ -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,