summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author TreeHugger Robot <treehugger-gerrit@google.com> 2020-11-03 22:11:25 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2020-11-03 22:11:25 +0000
commit8e5bcf841b2c8a75bad6c0906206317b89a57e47 (patch)
tree834775229b9d96fbe74b97254505bcf38c87bb15
parent3d19b61419ad89ad06f7d051ad2805bf9a65a0b8 (diff)
parentd3990155786d79796c7136da84df05999c16b1ab (diff)
Merge "Introduce public api for CombinedVibrationEffect"
-rw-r--r--core/java/android/os/CombinedVibrationEffect.java403
-rw-r--r--core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java121
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);