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

Commit 8e5bcf84 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introduce public api for CombinedVibrationEffect"

parents 3d19b614 d3990155
Loading
Loading
Loading
Loading
+398 −5
Original line number Diff line number Diff line
@@ -17,7 +17,12 @@
package android.os;

import android.annotation.NonNull;
import android.util.SparseArray;

import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
@@ -31,6 +36,8 @@ import java.util.Objects;
 */
public abstract class CombinedVibrationEffect implements Parcelable {
    private static final int PARCEL_TOKEN_MONO = 1;
    private static final int PARCEL_TOKEN_STEREO = 2;
    private static final int PARCEL_TOKEN_SEQUENTIAL = 3;

    /** @hide to prevent subclassing from outside of the framework */
    public CombinedVibrationEffect() {
@@ -41,8 +48,8 @@ public abstract class CombinedVibrationEffect implements Parcelable {
     *
     * A synced vibration effect should be performed by multiple vibrators at the same time.
     *
     * @param effect The {@link VibrationEffect} to perform
     * @return The desired combined effect.
     * @param effect The {@link VibrationEffect} to perform.
     * @return The synced effect.
     */
    @NonNull
    public static CombinedVibrationEffect createSynced(@NonNull VibrationEffect effect) {
@@ -51,6 +58,30 @@ public abstract class CombinedVibrationEffect implements Parcelable {
        return combined;
    }

    /**
     * Start creating a synced vibration effect.
     *
     * A synced vibration effect should be performed by multiple vibrators at the same time.
     *
     * @see CombinedVibrationEffect.SyncedCombination
     */
    @NonNull
    public static SyncedCombination startSynced() {
        return new SyncedCombination();
    }

    /**
     * Start creating a sequential vibration effect.
     *
     * A sequential vibration effect should be performed by multiple vibrators in order.
     *
     * @see CombinedVibrationEffect.SequentialCombination
     */
    @NonNull
    public static SequentialCombination startSequential() {
        return new SequentialCombination();
    }

    @Override
    public int describeContents() {
        return 0;
@@ -59,6 +90,164 @@ public abstract class CombinedVibrationEffect implements Parcelable {
    /** @hide */
    public abstract void validate();

    /**
     * A combination of haptic effects that should be played in multiple vibrators in sync.
     *
     * @hide
     * @see CombinedVibrationEffect#startSynced()
     */
    public static final class SyncedCombination {

        private final SparseArray<VibrationEffect> mEffects = new SparseArray<>();

        SyncedCombination() {
        }

        /**
         * Add or replace a one shot vibration effect to be performed by the specified vibrator.
         *
         * @param vibratorId The id of the vibrator that should perform this effect.
         * @param effect     The effect this vibrator should play.
         * @return The {@link CombinedVibrationEffect.SyncedCombination} object to enable adding
         * multiple effects in one chain.
         * @see VibrationEffect#createOneShot(long, int)
         */
        @NonNull
        public SyncedCombination addVibrator(int vibratorId, VibrationEffect effect) {
            mEffects.put(vibratorId, effect);
            return this;
        }

        /**
         * Combine all of the added effects into a combined effect.
         *
         * The {@link CombinedVibrationEffect.SyncedCombination} object is still valid after this
         * call, so you can continue adding more effects to it and generating more
         * {@link CombinedVibrationEffect}s by calling this method again.
         *
         * @return The {@link CombinedVibrationEffect} resulting from combining the added effects to
         * be played in sync.
         */
        @NonNull
        public CombinedVibrationEffect combine() {
            if (mEffects.size() == 0) {
                throw new IllegalStateException(
                        "Combination must have at least one element to combine.");
            }
            CombinedVibrationEffect combined = new Stereo(mEffects);
            combined.validate();
            return combined;
        }
    }

    /**
     * A combination of haptic effects that should be played in multiple vibrators in sequence.
     *
     * @hide
     * @see CombinedVibrationEffect#startSequential()
     */
    public static final class SequentialCombination {

        private final ArrayList<CombinedVibrationEffect> mEffects = new ArrayList<>();
        private final ArrayList<Integer> mDelays = new ArrayList<>();

        SequentialCombination() {
        }

        /**
         * Add a single vibration effect to be performed next.
         *
         * Similar to {@link #addNext(int, VibrationEffect, int)}, but with no delay.
         *
         * @param vibratorId The id of the vibrator that should perform this effect.
         * @param effect     The effect this vibrator should play.
         * @return The {@link CombinedVibrationEffect.SequentialCombination} object to enable adding
         * multiple effects in one chain.
         */
        @NonNull
        public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect) {
            return addNext(vibratorId, effect, /* delay= */ 0);
        }

        /**
         * Add a single vibration effect to be performed next.
         *
         * @param vibratorId The id of the vibrator that should perform this effect.
         * @param effect     The effect this vibrator should play.
         * @param delay      The amount of time, in milliseconds, to wait between playing the prior
         *                   effect and this one.
         * @return The {@link CombinedVibrationEffect.SequentialCombination} object to enable adding
         * multiple effects in one chain.
         */
        @NonNull
        public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect,
                int delay) {
            return addNext(
                    CombinedVibrationEffect.startSynced().addVibrator(vibratorId, effect).combine(),
                    delay);
        }

        /**
         * Add a combined vibration effect to be performed next.
         *
         * Similar to {@link #addNext(CombinedVibrationEffect, int)}, but with no delay.
         *
         * @param effect The combined effect to be performed next.
         * @return The {@link CombinedVibrationEffect.SequentialCombination} object to enable adding
         * multiple effects in one chain.
         * @see VibrationEffect#createOneShot(long, int)
         */
        @NonNull
        public SequentialCombination addNext(@NonNull CombinedVibrationEffect effect) {
            return addNext(effect, /* delay= */ 0);
        }

        /**
         * Add a one shot vibration effect to be performed by the specified vibrator.
         *
         * @param effect The combined effect to be performed next.
         * @param delay  The amount of time, in milliseconds, to wait between playing the prior
         *               effect and this one.
         * @return The {@link CombinedVibrationEffect.SequentialCombination} object to enable adding
         * multiple effects in one chain.
         */
        @NonNull
        public SequentialCombination addNext(@NonNull CombinedVibrationEffect effect, int delay) {
            if (effect instanceof Sequential) {
                Sequential sequentialEffect = (Sequential) effect;
                int firstEffectIndex = mDelays.size();
                mEffects.addAll(sequentialEffect.getEffects());
                mDelays.addAll(sequentialEffect.getDelays());
                mDelays.set(firstEffectIndex, delay + mDelays.get(firstEffectIndex));
            } else {
                mEffects.add(effect);
                mDelays.add(delay);
            }
            return this;
        }

        /**
         * Combine all of the added effects in sequence.
         *
         * The {@link CombinedVibrationEffect.SequentialCombination} object is still valid after
         * this call, so you can continue adding more effects to it and generating more {@link
         * CombinedVibrationEffect}s by calling this method again.
         *
         * @return The {@link CombinedVibrationEffect} resulting from combining the added effects to
         * be played in sequence.
         */
        @NonNull
        public CombinedVibrationEffect combine() {
            if (mEffects.size() == 0) {
                throw new IllegalStateException(
                        "Combination must have at least one element to combine.");
            }
            CombinedVibrationEffect combined = new Sequential(mEffects, mDelays);
            combined.validate();
            return combined;
        }
    }

    /**
     * Represents a single {@link VibrationEffect} that should be executed in all vibrators in sync.
     *
@@ -87,10 +276,10 @@ public abstract class CombinedVibrationEffect implements Parcelable {

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof CombinedVibrationEffect.Mono)) {
            if (!(o instanceof Mono)) {
                return false;
            }
            CombinedVibrationEffect.Mono other = (CombinedVibrationEffect.Mono) o;
            Mono other = (Mono) o;
            return other.mEffect.equals(other.mEffect);
        }

@@ -128,6 +317,206 @@ public abstract class CombinedVibrationEffect implements Parcelable {
                };
    }

    /**
     * Represents a list of {@link VibrationEffect}s that should be executed in sync.
     *
     * @hide
     */
    public static final class Stereo extends CombinedVibrationEffect {

        /** Mapping vibrator ids to effects. */
        private final SparseArray<VibrationEffect> mEffects;

        public Stereo(Parcel in) {
            int size = in.readInt();
            mEffects = new SparseArray<>(size);
            for (int i = 0; i < size; i++) {
                int vibratorId = in.readInt();
                mEffects.put(vibratorId, VibrationEffect.CREATOR.createFromParcel(in));
            }
        }

        public Stereo(@NonNull SparseArray<VibrationEffect> effects) {
            mEffects = new SparseArray<>(effects.size());
            for (int i = 0; i < effects.size(); i++) {
                mEffects.put(effects.keyAt(i), effects.valueAt(i));
            }
        }

        /** Effects to be performed in sync, where each key represents the vibrator id. */
        public SparseArray<VibrationEffect> getEffects() {
            return mEffects;
        }

        /** @hide */
        @Override
        public void validate() {
            Preconditions.checkArgument(mEffects.size() > 0,
                    "There should be at least one effect set for a combined effect");
            for (int i = 0; i < mEffects.size(); i++) {
                mEffects.valueAt(i).validate();
            }
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Stereo)) {
                return false;
            }
            Stereo other = (Stereo) o;
            if (mEffects.size() != other.mEffects.size()) {
                return false;
            }
            for (int i = 0; i < mEffects.size(); i++) {
                if (!mEffects.valueAt(i).equals(other.mEffects.get(mEffects.keyAt(i)))) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mEffects);
        }

        @Override
        public String toString() {
            return "Stereo{mEffects=" + mEffects + '}';
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(PARCEL_TOKEN_STEREO);
            out.writeInt(mEffects.size());
            for (int i = 0; i < mEffects.size(); i++) {
                out.writeInt(mEffects.keyAt(i));
                mEffects.valueAt(i).writeToParcel(out, flags);
            }
        }

        @NonNull
        public static final Parcelable.Creator<Stereo> CREATOR =
                new Parcelable.Creator<Stereo>() {
                    @Override
                    public Stereo createFromParcel(@NonNull Parcel in) {
                        // Skip the type token
                        in.readInt();
                        return new Stereo(in);
                    }

                    @Override
                    @NonNull
                    public Stereo[] newArray(int size) {
                        return new Stereo[size];
                    }
                };
    }

    /**
     * Represents a list of {@link VibrationEffect}s that should be executed in sequence.
     *
     * @hide
     */
    public static final class Sequential extends CombinedVibrationEffect {
        private final List<CombinedVibrationEffect> mEffects;
        private final List<Integer> mDelays;

        public Sequential(Parcel in) {
            int size = in.readInt();
            mEffects = new ArrayList<>(size);
            mDelays = new ArrayList<>(size);
            for (int i = 0; i < size; i++) {
                mDelays.add(in.readInt());
                mEffects.add(CombinedVibrationEffect.CREATOR.createFromParcel(in));
            }
        }

        public Sequential(@NonNull List<CombinedVibrationEffect> effects,
                @NonNull List<Integer> delays) {
            mEffects = new ArrayList<>(effects);
            mDelays = new ArrayList<>(delays);
        }

        /** Effects to be performed in sequence. */
        public List<CombinedVibrationEffect> getEffects() {
            return mEffects;
        }

        /** Delay to be applied before each effect in {@link #getEffects()}. */
        public List<Integer> getDelays() {
            return mDelays;
        }

        /** @hide */
        @Override
        public void validate() {
            Preconditions.checkArgument(mEffects.size() > 0,
                    "There should be at least one effect set for a combined effect");
            Preconditions.checkArgument(mEffects.size() == mDelays.size(),
                    "Effect and delays should have equal length");
            for (long delay : mDelays) {
                if (delay < 0) {
                    throw new IllegalArgumentException("Delays must all be >= 0"
                            + " (delays=" + mDelays + ")");
                }
            }
            for (CombinedVibrationEffect effect : mEffects) {
                if (effect instanceof Sequential) {
                    throw new IllegalArgumentException(
                            "There should be no nested sequential effects in a combined effect");
                }
                effect.validate();
            }
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Sequential)) {
                return false;
            }
            Sequential other = (Sequential) o;
            return mDelays.equals(other.mDelays) && mEffects.equals(other.mEffects);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mEffects);
        }

        @Override
        public String toString() {
            return "Sequential{mEffects=" + mEffects + ", mDelays=" + mDelays + '}';
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(PARCEL_TOKEN_SEQUENTIAL);
            out.writeInt(mEffects.size());
            for (int i = 0; i < mEffects.size(); i++) {
                out.writeInt(mDelays.get(i));
                mEffects.get(i).writeToParcel(out, flags);
            }
        }

        @NonNull
        public static final Parcelable.Creator<Sequential> CREATOR =
                new Parcelable.Creator<Sequential>() {
                    @Override
                    public Sequential createFromParcel(@NonNull Parcel in) {
                        // Skip the type token
                        in.readInt();
                        return new Sequential(in);
                    }

                    @Override
                    @NonNull
                    public Sequential[] newArray(int size) {
                        return new Sequential[size];
                    }
                };
    }

    @NonNull
    public static final Parcelable.Creator<CombinedVibrationEffect> CREATOR =
            new Parcelable.Creator<CombinedVibrationEffect>() {
@@ -135,7 +524,11 @@ public abstract class CombinedVibrationEffect implements Parcelable {
                public CombinedVibrationEffect createFromParcel(Parcel in) {
                    int token = in.readInt();
                    if (token == PARCEL_TOKEN_MONO) {
                        return new CombinedVibrationEffect.Mono(in);
                        return new Mono(in);
                    } else if (token == PARCEL_TOKEN_STEREO) {
                        return new Stereo(in);
                    } else if (token == PARCEL_TOKEN_SEQUENTIAL) {
                        return new Sequential(in);
                    } else {
                        throw new IllegalStateException(
                                "Unexpected combined vibration event type token in parcel.");
+117 −4
Original line number Diff line number Diff line
@@ -26,22 +26,135 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Arrays;

@Presubmit
@RunWith(JUnit4.class)
public class CombinedVibrationEffectTest {

    private static final VibrationEffect VALID_EFFECT = VibrationEffect.createOneShot(10, 255);
    private static final VibrationEffect INVALID_EFFECT = new VibrationEffect.OneShot(-1, -1);

    @Test
    public void testValidateMono() {
        CombinedVibrationEffect.createSynced(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
        CombinedVibrationEffect.createSynced(VALID_EFFECT);

        assertThrows(IllegalArgumentException.class,
                () -> CombinedVibrationEffect.createSynced(INVALID_EFFECT));
    }

    @Test
    public void testValidateStereo() {
        CombinedVibrationEffect.startSynced()
                .addVibrator(0, VALID_EFFECT)
                .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
                .combine();
        CombinedVibrationEffect.startSynced()
                .addVibrator(0, INVALID_EFFECT)
                .addVibrator(0, VALID_EFFECT)
                .combine();

        assertThrows(IllegalArgumentException.class,
                () -> CombinedVibrationEffect.startSynced()
                        .addVibrator(0, INVALID_EFFECT)
                        .combine());
    }

    @Test
    public void testValidateSequential() {
        CombinedVibrationEffect.startSequential()
                .addNext(0, VALID_EFFECT)
                .addNext(CombinedVibrationEffect.createSynced(VALID_EFFECT))
                .combine();
        CombinedVibrationEffect.startSequential()
                .addNext(0, VALID_EFFECT)
                .addNext(0, VALID_EFFECT, 100)
                .combine();
        CombinedVibrationEffect.startSequential()
                .addNext(CombinedVibrationEffect.startSequential()
                        .addNext(0, VALID_EFFECT)
                        .combine())
                .combine();

        assertThrows(IllegalArgumentException.class,
                () -> CombinedVibrationEffect.startSequential()
                        .addNext(0, VALID_EFFECT, -1)
                        .combine());
        assertThrows(IllegalArgumentException.class,
                () -> CombinedVibrationEffect.createSynced(new VibrationEffect.OneShot(-1, -1)));
                () -> CombinedVibrationEffect.startSequential()
                        .addNext(0, INVALID_EFFECT)
                        .combine());
        assertThrows(IllegalArgumentException.class,
                () -> new CombinedVibrationEffect.Sequential(
                        Arrays.asList(CombinedVibrationEffect.startSequential()
                                .addNext(CombinedVibrationEffect.createSynced(VALID_EFFECT))
                                .combine()),
                        Arrays.asList(0))
                        .validate());
    }

    @Test
    public void testNestedSequentialAccumulatesDelays() {
        CombinedVibrationEffect.Sequential combined =
                (CombinedVibrationEffect.Sequential) CombinedVibrationEffect.startSequential()
                        .addNext(CombinedVibrationEffect.startSequential()
                                        .addNext(0, VALID_EFFECT, /* delay= */ 100)
                                        .addNext(1, VALID_EFFECT, /* delay= */ 100)
                                        .combine(),
                                /* delay= */ 10)
                        .addNext(CombinedVibrationEffect.startSequential()
                                .addNext(0, VALID_EFFECT, /* delay= */ 100)
                                .combine())
                        .addNext(CombinedVibrationEffect.startSequential()
                                        .addNext(0, VALID_EFFECT)
                                        .addNext(0, VALID_EFFECT, /* delay= */ 100)
                                        .combine(),
                                /* delay= */ 10)
                        .combine();

        assertEquals(Arrays.asList(110, 100, 100, 10, 100), combined.getDelays());
    }

    @Test
    public void testCombineEmptyFails() {
        assertThrows(IllegalStateException.class,
                () -> CombinedVibrationEffect.startSynced().combine());
        assertThrows(IllegalStateException.class,
                () -> CombinedVibrationEffect.startSequential().combine());
    }

    @Test
    public void testSerializationMono() {
        CombinedVibrationEffect original = CombinedVibrationEffect.createSynced(
                VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
        CombinedVibrationEffect original = CombinedVibrationEffect.createSynced(VALID_EFFECT);

        Parcel parcel = Parcel.obtain();
        original.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        CombinedVibrationEffect restored = CombinedVibrationEffect.CREATOR.createFromParcel(parcel);
        assertEquals(original, restored);
    }

    @Test
    public void testSerializationStereo() {
        CombinedVibrationEffect original = CombinedVibrationEffect.startSynced()
                .addVibrator(0, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                .addVibrator(1, VibrationEffect.createOneShot(10, 255))
                .combine();

        Parcel parcel = Parcel.obtain();
        original.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        CombinedVibrationEffect restored = CombinedVibrationEffect.CREATOR.createFromParcel(parcel);
        assertEquals(original, restored);
    }

    @Test
    public void testSerializationSequential() {
        CombinedVibrationEffect original = CombinedVibrationEffect.startSequential()
                .addNext(0, VALID_EFFECT)
                .addNext(CombinedVibrationEffect.createSynced(VALID_EFFECT))
                .addNext(0, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), 100)
                .combine();

        Parcel parcel = Parcel.obtain();
        original.writeToParcel(parcel, 0);