diff options
author | 2020-11-03 22:11:25 +0000 | |
---|---|---|
committer | 2020-11-03 22:11:25 +0000 | |
commit | 8e5bcf841b2c8a75bad6c0906206317b89a57e47 (patch) | |
tree | 834775229b9d96fbe74b97254505bcf38c87bb15 | |
parent | 3d19b61419ad89ad06f7d051ad2805bf9a65a0b8 (diff) | |
parent | d3990155786d79796c7136da84df05999c16b1ab (diff) |
Merge "Introduce public api for CombinedVibrationEffect"
-rw-r--r-- | core/java/android/os/CombinedVibrationEffect.java | 403 | ||||
-rw-r--r-- | core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java | 121 |
2 files changed, 515 insertions, 9 deletions
diff --git a/core/java/android/os/CombinedVibrationEffect.java b/core/java/android/os/CombinedVibrationEffect.java index 77bfa577babd..f552aaa55796 100644 --- a/core/java/android/os/CombinedVibrationEffect.java +++ b/core/java/android/os/CombinedVibrationEffect.java @@ -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; @@ -60,6 +91,164 @@ public abstract class CombinedVibrationEffect implements Parcelable { 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. * * @hide @@ -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."); diff --git a/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java b/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java index faa67a8bbd62..1947c6cf8ca0 100644 --- a/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java @@ -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); |