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

Commit c04d8de7 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit Committed by Android (Google) Code Review
Browse files

Merge "Create API to Compute Vibration Pattern Equivalent to a VibrationEffect"

parents 91c07d22 4e125b21
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,