Loading core/java/android/os/CombinedVibrationEffect.java +398 −5 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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() { Loading @@ -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) { Loading @@ -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; Loading @@ -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. * Loading Loading @@ -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); } Loading Loading @@ -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>() { Loading @@ -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."); Loading core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java +117 −4 Original line number Diff line number Diff line Loading @@ -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); Loading Loading
core/java/android/os/CombinedVibrationEffect.java +398 −5 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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() { Loading @@ -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) { Loading @@ -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; Loading @@ -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. * Loading Loading @@ -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); } Loading Loading @@ -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>() { Loading @@ -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."); Loading
core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java +117 −4 Original line number Diff line number Diff line Loading @@ -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); Loading