Introduce public api for CombinedVibrationEffect

Introducing stereo effects and synced waveform effects.

More at go/android-s-vibratormanager

Fix: 166586119
Test: atest FrameworksCoreTest:CombinedVibrationEffectTest
Change-Id: Ic567b6470b967474066e6d087c619148af9cc72d
This commit is contained in:
Lais Andrade
2020-10-06 18:24:24 +00:00
parent a2a253995d
commit d399015578
2 changed files with 515 additions and 9 deletions

View File

@@ -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.");

View File

@@ -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(new VibrationEffect.OneShot(-1, -1)));
() -> 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.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);