Introduce VibrationEffect.createVendorEffect
Introduce new @SystemApi for vendor-specific vibration effects.
Fix: 345405987
Flag: android.os.vibrator.vendor_vibration_effects
Test: FrameworksVibratorCoreTests
FrameworksVibratorServicesTests
CtsVibratorTestCases
Change-Id: I86e15e495196330c32723618c917a4f6993c0d45
diff --git a/Android.bp b/Android.bp
index f0aa62c..115e5e8 100644
--- a/Android.bp
+++ b/Android.bp
@@ -255,7 +255,7 @@
"android.hardware.vibrator-V1.1-java",
"android.hardware.vibrator-V1.2-java",
"android.hardware.vibrator-V1.3-java",
- "android.hardware.vibrator-V2-java",
+ "android.hardware.vibrator-V3-java",
"android.se.omapi-V1-java",
"android.system.suspend.control.internal-java",
"devicepolicyprotosnano",
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 36a335e..fd0262e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -409,6 +409,7 @@
field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE";
field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED";
+ field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String VIBRATE_VENDOR_EFFECTS = "android.permission.VIBRATE_VENDOR_EFFECTS";
field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS";
field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS";
field public static final String WIFI_ACCESS_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS";
@@ -11354,6 +11355,10 @@
field @NonNull public static final android.os.Parcelable.Creator<android.os.UserManager.EnforcingUser> CREATOR;
}
+ public abstract class VibrationEffect implements android.os.Parcelable {
+ method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") @NonNull @RequiresPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS) public static android.os.VibrationEffect createVendorEffect(@NonNull android.os.PersistableBundle);
+ }
+
public abstract class Vibrator {
method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener);
method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.Vibrator.OnVibratorStateChangedListener);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 88b5275..90af259 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2577,6 +2577,16 @@
public static final class VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException extends java.lang.IllegalStateException {
}
+ @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final class VibrationEffect.VendorEffect extends android.os.VibrationEffect {
+ method @Nullable public long[] computeCreateWaveformOffOnTimingsOrNull();
+ method public long getDuration();
+ method public int getEffectStrength();
+ method public float getLinearScale();
+ method @NonNull public android.os.PersistableBundle getVendorData();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect.VendorEffect> CREATOR;
+ }
+
public static class VibrationEffect.VibrationParameter {
method @NonNull public static android.os.VibrationEffect.VibrationParameter targetAmplitude(@FloatRange(from=0, to=1) float);
method @NonNull public static android.os.VibrationEffect.VibrationParameter targetFrequency(@FloatRange(from=1) float);
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java
index f32a1f8..77d6cb7 100644
--- a/core/java/android/os/CombinedVibration.java
+++ b/core/java/android/os/CombinedVibration.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.TestApi;
+import android.os.vibrator.Flags;
import android.util.SparseArray;
import com.android.internal.util.Preconditions;
@@ -152,6 +153,9 @@
/** @hide */
public abstract boolean hasVibrator(int vibratorId);
+ /** @hide */
+ public abstract boolean hasVendorEffects();
+
/**
* Returns a compact version of the {@link #toString()} result for debugging purposes.
*
@@ -424,6 +428,15 @@
return true;
}
+ /** @hide */
+ @Override
+ public boolean hasVendorEffects() {
+ if (!Flags.vendorVibrationEffects()) {
+ return false;
+ }
+ return mEffect instanceof VibrationEffect.VendorEffect;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -605,6 +618,20 @@
return mEffects.indexOfKey(vibratorId) >= 0;
}
+ /** @hide */
+ @Override
+ public boolean hasVendorEffects() {
+ if (!Flags.vendorVibrationEffects()) {
+ return false;
+ }
+ for (int i = 0; i < mEffects.size(); i++) {
+ if (mEffects.get(i) instanceof VibrationEffect.VendorEffect) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -838,6 +865,17 @@
return false;
}
+ /** @hide */
+ @Override
+ public boolean hasVendorEffects() {
+ for (int i = 0; i < mEffects.size(); i++) {
+ if (mEffects.get(i).hasVendorEffects()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index efbd96b..44edf29 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -16,18 +16,25 @@
package android.os;
+import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS;
+
+import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.content.Context;
+import android.hardware.vibrator.IVibrator;
import android.hardware.vibrator.V1_0.EffectStrength;
import android.hardware.vibrator.V1_3.Effect;
import android.net.Uri;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
@@ -46,6 +53,7 @@
import java.util.Locale;
import java.util.Objects;
import java.util.StringJoiner;
+import java.util.function.BiFunction;
/**
* A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}.
@@ -53,6 +61,9 @@
* <p>These effects may be any number of things, from single shot vibrations to complex waveforms.
*/
public abstract class VibrationEffect implements Parcelable {
+ private static final int PARCEL_TOKEN_COMPOSED = 1;
+ private static final int PARCEL_TOKEN_VENDOR_EFFECT = 2;
+
// Stevens' coefficient to scale the perceived vibration intensity.
private static final float SCALE_GAMMA = 0.65f;
// If a vibration is playing for longer than 1s, it's probably not haptic feedback
@@ -316,6 +327,28 @@
}
/**
+ * Create a vendor-defined vibration effect.
+ *
+ * <p>Vendor effects offer more flexibility for accessing vendor-specific vibrator capabilities,
+ * enabling control over any vibration parameter and more generic vibration waveforms for apps
+ * provided by the device vendor.
+ *
+ * <p>This requires hardware-specific implementation of the effect and will not have any
+ * platform fallback support.
+ *
+ * @param effect An opaque representation of the vibration effect which can also be serialized.
+ * @return The desired effect.
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ @FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS)
+ @RequiresPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)
+ public static VibrationEffect createVendorEffect(@NonNull PersistableBundle effect) {
+ return new VendorEffect(effect, VendorEffect.DEFAULT_STRENGTH, VendorEffect.DEFAULT_SCALE);
+ }
+
+ /**
* Get a predefined vibration effect.
*
* <p>Predefined effects are a set of common vibration effects that should be identical,
@@ -508,7 +541,7 @@
* Gets the estimated duration of the vibration in milliseconds.
*
* <p>For effects without a defined end (e.g. a Waveform with a non-negative repeat index), this
- * returns Long.MAX_VALUE. For effects with an unknown duration (e.g. Prebaked effects where
+ * returns Long.MAX_VALUE. For effects with an unknown duration (e.g. predefined effects where
* the length is device and potentially run-time dependent), this returns -1.
*
* @hide
@@ -550,7 +583,19 @@
*
* @hide
*/
- public abstract <T extends VibrationEffect> T resolve(int defaultAmplitude);
+ @NonNull
+ public abstract VibrationEffect resolve(int defaultAmplitude);
+
+ /**
+ * Applies given effect strength to predefined and vendor-specific effects.
+ *
+ * @param effectStrength new effect strength to be applied, one of
+ * VibrationEffect.EFFECT_STRENGTH_*.
+ * @return this if there is no change, or a copy of this effect with new strength otherwise
+ * @hide
+ */
+ @NonNull
+ public abstract VibrationEffect applyEffectStrength(int effectStrength);
/**
* Scale the vibration effect intensity with the given constraints.
@@ -562,7 +607,20 @@
*
* @hide
*/
- public abstract <T extends VibrationEffect> T scale(float scaleFactor);
+ @NonNull
+ public abstract VibrationEffect scale(float scaleFactor);
+
+ /**
+ * Performs a linear scaling on the effect intensity with the given factor.
+ *
+ * @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will
+ * scale down the intensity, values larger than 1 will scale up
+ * @return this if there is no scaling to be done, or a copy of this effect with scaled
+ * vibration intensity otherwise
+ * @hide
+ */
+ @NonNull
+ public abstract VibrationEffect scaleLinearly(float scaleFactor);
/**
* Ensures that the effect is repeating indefinitely or not. This is a lossy operation and
@@ -651,38 +709,26 @@
/** @hide */
public static String effectIdToString(int effectId) {
- switch (effectId) {
- case EFFECT_CLICK:
- return "CLICK";
- case EFFECT_TICK:
- return "TICK";
- case EFFECT_HEAVY_CLICK:
- return "HEAVY_CLICK";
- case EFFECT_DOUBLE_CLICK:
- return "DOUBLE_CLICK";
- case EFFECT_POP:
- return "POP";
- case EFFECT_THUD:
- return "THUD";
- case EFFECT_TEXTURE_TICK:
- return "TEXTURE_TICK";
- default:
- return Integer.toString(effectId);
- }
+ return switch (effectId) {
+ case EFFECT_CLICK -> "CLICK";
+ case EFFECT_TICK -> "TICK";
+ case EFFECT_HEAVY_CLICK -> "HEAVY_CLICK";
+ case EFFECT_DOUBLE_CLICK -> "DOUBLE_CLICK";
+ case EFFECT_POP -> "POP";
+ case EFFECT_THUD -> "THUD";
+ case EFFECT_TEXTURE_TICK -> "TEXTURE_TICK";
+ default -> Integer.toString(effectId);
+ };
}
/** @hide */
public static String effectStrengthToString(int effectStrength) {
- switch (effectStrength) {
- case EFFECT_STRENGTH_LIGHT:
- return "LIGHT";
- case EFFECT_STRENGTH_MEDIUM:
- return "MEDIUM";
- case EFFECT_STRENGTH_STRONG:
- return "STRONG";
- default:
- return Integer.toString(effectStrength);
- }
+ return switch (effectStrength) {
+ case EFFECT_STRENGTH_LIGHT -> "LIGHT";
+ case EFFECT_STRENGTH_MEDIUM -> "MEDIUM";
+ case EFFECT_STRENGTH_STRONG -> "STRONG";
+ default -> Integer.toString(effectStrength);
+ };
}
/**
@@ -712,12 +758,15 @@
private final ArrayList<VibrationEffectSegment> mSegments;
private final int mRepeatIndex;
+ /** @hide */
Composed(@NonNull Parcel in) {
- this(in.readArrayList(
- VibrationEffectSegment.class.getClassLoader(), VibrationEffectSegment.class),
+ this(Objects.requireNonNull(in.readArrayList(
+ VibrationEffectSegment.class.getClassLoader(),
+ VibrationEffectSegment.class)),
in.readInt());
}
+ /** @hide */
Composed(@NonNull VibrationEffectSegment segment) {
this(Arrays.asList(segment), /* repeatIndex= */ -1);
}
@@ -844,7 +893,7 @@
}
int segmentCount = mSegments.size();
if (segmentCount > MAX_HAPTIC_FEEDBACK_COMPOSITION_SIZE) {
- // Vibration has some prebaked or primitive constants, it should be limited to the
+ // Vibration has some predefined or primitive constants, it should be limited to the
// max composition size used to classify haptic feedbacks.
return false;
}
@@ -867,34 +916,28 @@
@NonNull
@Override
public Composed resolve(int defaultAmplitude) {
- int segmentCount = mSegments.size();
- ArrayList<VibrationEffectSegment> resolvedSegments = new ArrayList<>(segmentCount);
- for (int i = 0; i < segmentCount; i++) {
- resolvedSegments.add(mSegments.get(i).resolve(defaultAmplitude));
- }
- if (resolvedSegments.equals(mSegments)) {
- return this;
- }
- Composed resolved = new Composed(resolvedSegments, mRepeatIndex);
- resolved.validate();
- return resolved;
+ return applyToSegments(VibrationEffectSegment::resolve, defaultAmplitude);
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public VibrationEffect applyEffectStrength(int effectStrength) {
+ return applyToSegments(VibrationEffectSegment::applyEffectStrength, effectStrength);
}
/** @hide */
@NonNull
@Override
public Composed scale(float scaleFactor) {
- int segmentCount = mSegments.size();
- ArrayList<VibrationEffectSegment> scaledSegments = new ArrayList<>(segmentCount);
- for (int i = 0; i < segmentCount; i++) {
- scaledSegments.add(mSegments.get(i).scale(scaleFactor));
- }
- if (scaledSegments.equals(mSegments)) {
- return this;
- }
- Composed scaled = new Composed(scaledSegments, mRepeatIndex);
- scaled.validate();
- return scaled;
+ return applyToSegments(VibrationEffectSegment::scale, scaleFactor);
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public Composed scaleLinearly(float scaleFactor) {
+ return applyToSegments(VibrationEffectSegment::scaleLinearly, scaleFactor);
}
/** @hide */
@@ -926,10 +969,9 @@
if (this == o) {
return true;
}
- if (!(o instanceof Composed)) {
+ if (!(o instanceof Composed other)) {
return false;
}
- Composed other = (Composed) o;
return mSegments.equals(other.mSegments) && mRepeatIndex == other.mRepeatIndex;
}
@@ -969,6 +1011,7 @@
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(PARCEL_TOKEN_COMPOSED);
out.writeList(mSegments);
out.writeInt(mRepeatIndex);
}
@@ -1011,6 +1054,208 @@
return stepSegment;
}
+
+ private <T> Composed applyToSegments(
+ BiFunction<VibrationEffectSegment, T, VibrationEffectSegment> function, T param) {
+ int segmentCount = mSegments.size();
+ ArrayList<VibrationEffectSegment> updatedSegments = new ArrayList<>(segmentCount);
+ for (int i = 0; i < segmentCount; i++) {
+ updatedSegments.add(function.apply(mSegments.get(i), param));
+ }
+ if (mSegments.equals(updatedSegments)) {
+ return this;
+ }
+ Composed updated = new Composed(updatedSegments, mRepeatIndex);
+ updated.validate();
+ return updated;
+ }
+ }
+
+ /**
+ * Implementation of {@link VibrationEffect} described by a generic {@link PersistableBundle}
+ * defined by vendors.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS)
+ public static final class VendorEffect extends VibrationEffect {
+ /** @hide */
+ public static final int DEFAULT_STRENGTH = VibrationEffect.EFFECT_STRENGTH_MEDIUM;
+ /** @hide */
+ public static final float DEFAULT_SCALE = 1.0f;
+
+ private final PersistableBundle mVendorData;
+ private final int mEffectStrength;
+ private final float mLinearScale;
+
+ /** @hide */
+ VendorEffect(@NonNull Parcel in) {
+ this(Objects.requireNonNull(
+ in.readPersistableBundle(VibrationEffect.class.getClassLoader())),
+ in.readInt(), in.readFloat());
+ }
+
+ /** @hide */
+ public VendorEffect(@NonNull PersistableBundle vendorData, int effectStrength,
+ float linearScale) {
+ mVendorData = vendorData;
+ mEffectStrength = effectStrength;
+ mLinearScale = linearScale;
+ }
+
+ @NonNull
+ public PersistableBundle getVendorData() {
+ return mVendorData;
+ }
+
+ public int getEffectStrength() {
+ return mEffectStrength;
+ }
+
+ public float getLinearScale() {
+ return mLinearScale;
+ }
+
+ /** @hide */
+ @Override
+ @Nullable
+ public long[] computeCreateWaveformOffOnTimingsOrNull() {
+ return null;
+ }
+
+ /** @hide */
+ @Override
+ public void validate() {
+ Preconditions.checkArgument(!mVendorData.isEmpty(),
+ "Vendor effect bundle must be non-empty");
+ }
+
+ @Override
+ public long getDuration() {
+ return -1; // UNKNOWN
+ }
+
+ /** @hide */
+ @Override
+ public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
+ return vibratorInfo.hasCapability(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+ }
+
+ /** @hide */
+ @Override
+ public boolean isHapticFeedbackCandidate() {
+ return false;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public VendorEffect resolve(int defaultAmplitude) {
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public VibrationEffect applyEffectStrength(int effectStrength) {
+ if (mEffectStrength == effectStrength) {
+ return this;
+ }
+ VendorEffect updated = new VendorEffect(mVendorData, effectStrength, mLinearScale);
+ updated.validate();
+ return updated;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public VendorEffect scale(float scaleFactor) {
+ // Vendor effect strength cannot be scaled with this method.
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public VibrationEffect scaleLinearly(float scaleFactor) {
+ if (Float.compare(mLinearScale, scaleFactor) == 0) {
+ return this;
+ }
+ VendorEffect updated = new VendorEffect(mVendorData, mEffectStrength, scaleFactor);
+ updated.validate();
+ return updated;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public VendorEffect applyRepeatingIndefinitely(boolean wantRepeating, int loopDelayMs) {
+ return this;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof VendorEffect other)) {
+ return false;
+ }
+ return mEffectStrength == other.mEffectStrength
+ && (Float.compare(mLinearScale, other.mLinearScale) == 0)
+ // Make sure it calls unparcel for both before calling BaseBundle.kindofEquals.
+ && mVendorData.size() == other.mVendorData.size()
+ && BaseBundle.kindofEquals(mVendorData, other.mVendorData);
+ }
+
+ @Override
+ public int hashCode() {
+ // PersistableBundle does not implement hashCode, so use its size as a shortcut.
+ return Objects.hash(mVendorData.size(), mEffectStrength, mLinearScale);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ROOT,
+ "VendorEffect{vendorData=%s, strength=%s, scale=%.2f}",
+ mVendorData, effectStrengthToString(mEffectStrength), mLinearScale);
+ }
+
+ /** @hide */
+ @Override
+ public String toDebugString() {
+ return String.format(Locale.ROOT, "vendorEffect=%s, strength=%s, scale=%.2f",
+ mVendorData.toShortString(), effectStrengthToString(mEffectStrength),
+ mLinearScale);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(PARCEL_TOKEN_VENDOR_EFFECT);
+ out.writePersistableBundle(mVendorData);
+ out.writeInt(mEffectStrength);
+ out.writeFloat(mLinearScale);
+ }
+
+ @NonNull
+ public static final Creator<VendorEffect> CREATOR =
+ new Creator<VendorEffect>() {
+ @Override
+ public VendorEffect createFromParcel(Parcel in) {
+ return new VendorEffect(in);
+ }
+
+ @Override
+ public VendorEffect[] newArray(int size) {
+ return new VendorEffect[size];
+ }
+ };
}
/**
@@ -1249,7 +1494,9 @@
if (mRepeatIndex >= 0) {
throw new UnreachableAfterRepeatingIndefinitelyException();
}
- Composed composed = (Composed) effect;
+ if (!(effect instanceof Composed composed)) {
+ throw new IllegalArgumentException("Can't add vendor effects to composition.");
+ }
if (composed.getRepeatIndex() >= 0) {
// Start repeating from the index relative to the composed waveform.
mRepeatIndex = mSegments.size() + composed.getRepeatIndex();
@@ -1285,28 +1532,18 @@
* @hide
*/
public static String primitiveToString(@PrimitiveType int id) {
- switch (id) {
- case PRIMITIVE_NOOP:
- return "NOOP";
- case PRIMITIVE_CLICK:
- return "CLICK";
- case PRIMITIVE_THUD:
- return "THUD";
- case PRIMITIVE_SPIN:
- return "SPIN";
- case PRIMITIVE_QUICK_RISE:
- return "QUICK_RISE";
- case PRIMITIVE_SLOW_RISE:
- return "SLOW_RISE";
- case PRIMITIVE_QUICK_FALL:
- return "QUICK_FALL";
- case PRIMITIVE_TICK:
- return "TICK";
- case PRIMITIVE_LOW_TICK:
- return "LOW_TICK";
- default:
- return Integer.toString(id);
- }
+ return switch (id) {
+ case PRIMITIVE_NOOP -> "NOOP";
+ case PRIMITIVE_CLICK -> "CLICK";
+ case PRIMITIVE_THUD -> "THUD";
+ case PRIMITIVE_SPIN -> "SPIN";
+ case PRIMITIVE_QUICK_RISE -> "QUICK_RISE";
+ case PRIMITIVE_SLOW_RISE -> "SLOW_RISE";
+ case PRIMITIVE_QUICK_FALL -> "QUICK_FALL";
+ case PRIMITIVE_TICK -> "TICK";
+ case PRIMITIVE_LOW_TICK -> "LOW_TICK";
+ default -> Integer.toString(id);
+ };
}
}
@@ -1640,7 +1877,17 @@
new Parcelable.Creator<VibrationEffect>() {
@Override
public VibrationEffect createFromParcel(Parcel in) {
- return new Composed(in);
+ switch (in.readInt()) {
+ case PARCEL_TOKEN_COMPOSED:
+ return new Composed(in);
+ case PARCEL_TOKEN_VENDOR_EFFECT:
+ if (Flags.vendorVibrationEffects()) {
+ return new VendorEffect(in);
+ } // else fall through
+ default:
+ throw new IllegalStateException(
+ "Unexpected vibration effect type token in parcel.");
+ }
}
@Override
public VibrationEffect[] newArray(int size) {
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index ad2f59d..f4e2a7e 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -53,3 +53,14 @@
purpose: PURPOSE_FEATURE
}
}
+
+flag {
+ namespace: "haptics"
+ name: "vendor_vibration_effects"
+ is_exported: true
+ description: "Enabled System APIs for vendor-defined vibration effects"
+ bug: "345454923"
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 9f00d5e..b6c2733 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2611,6 +2611,14 @@
<permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS"
android:protectionLevel="signature" />
+ <!-- @SystemApi Allows access to perform vendor effects in the vibrator.
+ <p>Protection level: signature
+ @FlaggedApi("android.os.vibrator.vendor_vibration_effects")
+ @hide
+ -->
+ <permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi Allows access to the vibrator state.
<p>Protection level: signature
@hide
diff --git a/core/tests/vibrator/Android.bp b/core/tests/vibrator/Android.bp
index 3ebe150..920ab59 100644
--- a/core/tests/vibrator/Android.bp
+++ b/core/tests/vibrator/Android.bp
@@ -18,6 +18,7 @@
"androidx.test.ext.junit",
"androidx.test.runner",
"androidx.test.rules",
+ "flag-junit",
"mockito-target-minus-junit4",
"truth",
"testng",
diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
index e875875..098ade4 100644
--- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java
+++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
@@ -20,6 +20,8 @@
import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
@@ -29,6 +31,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertThrows;
import android.content.ContentInterface;
@@ -38,8 +41,12 @@
import android.hardware.vibrator.IVibrator;
import android.net.Uri;
import android.os.VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import com.android.internal.R;
@@ -284,10 +291,13 @@
}
@Test
- public void computeLegacyPattern_notPatternPased() {
- VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
-
- assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
+ public void computeLegacyPattern_notPatternBased() {
+ assertNull(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
+ .computeCreateWaveformOffOnTimingsOrNull());
+ if (Flags.vendorVibrationEffects()) {
+ assertNull(VibrationEffect.createVendorEffect(createNonEmptyBundle())
+ .computeCreateWaveformOffOnTimingsOrNull());
+ }
}
@Test
@@ -472,6 +482,18 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testValidateVendorEffect() {
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("key", 1);
+ VibrationEffect.createVendorEffect(vendorData).validate();
+
+ PersistableBundle emptyData = new PersistableBundle();
+ assertThrows(IllegalArgumentException.class,
+ () -> VibrationEffect.createVendorEffect(emptyData).validate());
+ }
+
+ @Test
public void testValidateWaveform() {
VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1).validate();
VibrationEffect.createWaveform(new long[]{10, 10}, new int[] {0, 0}, -1).validate();
@@ -634,16 +656,16 @@
@Test
public void testResolveOneShot() {
- VibrationEffect.Composed resolved = DEFAULT_ONE_SHOT.resolve(51);
- assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude());
+ VibrationEffect resolved = DEFAULT_ONE_SHOT.resolve(51);
+ assertEquals(0.2f, getStepSegment(resolved, 0).getAmplitude());
assertThrows(IllegalArgumentException.class, () -> DEFAULT_ONE_SHOT.resolve(1000));
}
@Test
public void testResolveWaveform() {
- VibrationEffect.Composed resolved = TEST_WAVEFORM.resolve(102);
- assertEquals(0.4f, ((StepSegment) resolved.getSegments().get(2)).getAmplitude());
+ VibrationEffect resolved = TEST_WAVEFORM.resolve(102);
+ assertEquals(0.4f, getStepSegment(resolved, 2).getAmplitude());
assertThrows(IllegalArgumentException.class, () -> TEST_WAVEFORM.resolve(1000));
}
@@ -655,63 +677,127 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testResolveVendorEffect() {
+ VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle());
+ assertEquals(effect, effect.resolve(51));
+ }
+
+ @Test
public void testResolveComposed() {
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 1)
.compose();
assertEquals(effect, effect.resolve(51));
- VibrationEffect.Composed resolved = VibrationEffect.startComposition()
+ VibrationEffect resolved = VibrationEffect.startComposition()
.addEffect(DEFAULT_ONE_SHOT)
.compose()
.resolve(51);
- assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude());
+ assertEquals(0.2f, getStepSegment(resolved, 0).getAmplitude());
}
@Test
public void testScaleOneShot() {
- VibrationEffect.Composed scaledUp = TEST_ONE_SHOT.scale(1.5f);
- assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude());
+ VibrationEffect scaledUp = TEST_ONE_SHOT.scale(1.5f);
+ assertTrue(100 / 255f < getStepSegment(scaledUp, 0).getAmplitude());
- VibrationEffect.Composed scaledDown = TEST_ONE_SHOT.scale(0.5f);
- assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude());
+ VibrationEffect scaledDown = TEST_ONE_SHOT.scale(0.5f);
+ assertTrue(100 / 255f > getStepSegment(scaledDown, 0).getAmplitude());
}
@Test
public void testScaleWaveform() {
- VibrationEffect.Composed scaledUp = TEST_WAVEFORM.scale(1.5f);
- assertEquals(1f, ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude(), 1e-5f);
+ VibrationEffect scaledUp = TEST_WAVEFORM.scale(1.5f);
+ assertEquals(1f, getStepSegment(scaledUp, 0).getAmplitude(), 1e-5f);
- VibrationEffect.Composed scaledDown = TEST_WAVEFORM.scale(0.5f);
- assertTrue(1f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude());
+ VibrationEffect scaledDown = TEST_WAVEFORM.scale(0.5f);
+ assertTrue(1f > getStepSegment(scaledDown, 0).getAmplitude());
}
@Test
public void testScalePrebaked() {
VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
- VibrationEffect.Composed scaledUp = effect.scale(1.5f);
+ VibrationEffect scaledUp = effect.scale(1.5f);
assertEquals(effect, scaledUp);
- VibrationEffect.Composed scaledDown = effect.scale(0.5f);
+ VibrationEffect scaledDown = effect.scale(0.5f);
+ assertEquals(effect, scaledDown);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testScaleVendorEffect() {
+ VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle());
+
+ VibrationEffect scaledUp = effect.scale(1.5f);
+ assertEquals(effect, scaledUp);
+
+ VibrationEffect scaledDown = effect.scale(0.5f);
assertEquals(effect, scaledDown);
}
@Test
public void testScaleComposed() {
- VibrationEffect.Composed effect =
- (VibrationEffect.Composed) VibrationEffect.startComposition()
+ VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1)
.addEffect(TEST_ONE_SHOT)
.compose();
- VibrationEffect.Composed scaledUp = effect.scale(1.5f);
- assertTrue(0.5f < ((PrimitiveSegment) scaledUp.getSegments().get(0)).getScale());
- assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(1)).getAmplitude());
+ VibrationEffect scaledUp = effect.scale(1.5f);
+ assertTrue(0.5f < getPrimitiveSegment(scaledUp, 0).getScale());
+ assertTrue(100 / 255f < getStepSegment(scaledUp, 1).getAmplitude());
- VibrationEffect.Composed scaledDown = effect.scale(0.5f);
- assertTrue(0.5f > ((PrimitiveSegment) scaledDown.getSegments().get(0)).getScale());
- assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(1)).getAmplitude());
+ VibrationEffect scaledDown = effect.scale(0.5f);
+ assertTrue(0.5f > getPrimitiveSegment(scaledDown, 0).getScale());
+ assertTrue(100 / 255f > getStepSegment(scaledDown, 1).getAmplitude());
+ }
+
+ @Test
+ public void testApplyEffectStrengthToOneShotWaveformAndPrimitives() {
+ VibrationEffect oneShot = VibrationEffect.createOneShot(100, 100);
+ VibrationEffect waveform = VibrationEffect.createWaveform(new long[] { 10, 20 }, 0);
+ VibrationEffect composition = VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+ .compose();
+
+ assertEquals(oneShot, oneShot.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG));
+ assertEquals(waveform,
+ waveform.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG));
+ assertEquals(composition,
+ composition.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG));
+ }
+
+ @Test
+ public void testApplyEffectStrengthToPredefinedEffect() {
+ VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+
+ VibrationEffect scaledUp =
+ effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG);
+ assertNotEquals(effect, scaledUp);
+ assertEquals(VibrationEffect.EFFECT_STRENGTH_STRONG,
+ getPrebakedSegment(scaledUp, 0).getEffectStrength());
+
+ VibrationEffect scaledDown =
+ effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT);
+ assertNotEquals(effect, scaledDown);
+ assertEquals(VibrationEffect.EFFECT_STRENGTH_LIGHT,
+ getPrebakedSegment(scaledDown, 0).getEffectStrength());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testApplyEffectStrengthToVendorEffect() {
+ VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle());
+
+ VibrationEffect scaledUp =
+ effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG);
+ assertNotEquals(effect, scaledUp);
+
+ VibrationEffect scaledDown =
+ effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT);
+ assertNotEquals(effect, scaledDown);
}
private void doTestApplyRepeatingWithNonRepeatingOriginal(@NotNull VibrationEffect original) {
@@ -819,6 +905,15 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testApplyRepeatingIndefinitely_vendorEffect() {
+ VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle());
+
+ assertEquals(effect, effect.applyRepeatingIndefinitely(true, 10));
+ assertEquals(effect, effect.applyRepeatingIndefinitely(false, 10));
+ }
+
+ @Test
public void testDuration() {
assertEquals(1, VibrationEffect.createOneShot(1, 1).getDuration());
assertEquals(-1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK).getDuration());
@@ -832,6 +927,10 @@
new long[]{1, 2, 3}, new int[]{1, 2, 3}, -1).getDuration());
assertEquals(Long.MAX_VALUE, VibrationEffect.createWaveform(
new long[]{1, 2, 3}, new int[]{1, 2, 3}, 0).getDuration());
+ if (Flags.vendorVibrationEffects()) {
+ assertEquals(-1,
+ VibrationEffect.createVendorEffect(createNonEmptyBundle()).getDuration());
+ }
}
@Test
@@ -872,6 +971,19 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testAreVibrationFeaturesSupported_vendorEffects() {
+ VibratorInfo supportedVibratorInfo = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS)
+ .build();
+
+ assertTrue(VibrationEffect.createVendorEffect(createNonEmptyBundle())
+ .areVibrationFeaturesSupported(supportedVibratorInfo));
+ assertFalse(VibrationEffect.createVendorEffect(createNonEmptyBundle())
+ .areVibrationFeaturesSupported(new VibratorInfo.Builder(/* id= */ 1).build()));
+ }
+
+ @Test
public void testIsHapticFeedbackCandidate_repeatingEffects_notCandidates() {
assertFalse(VibrationEffect.createWaveform(
new long[]{1, 2, 3}, new int[]{1, 2, 3}, 0).isHapticFeedbackCandidate());
@@ -952,6 +1064,13 @@
assertTrue(VibrationEffect.get(VibrationEffect.EFFECT_TICK).isHapticFeedbackCandidate());
}
+ @Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testIsHapticFeedbackCandidate_vendorEffects_notCandidates() {
+ assertFalse(VibrationEffect.createVendorEffect(createNonEmptyBundle())
+ .isHapticFeedbackCandidate());
+ }
+
private void assertArrayEq(long[] expected, long[] actual) {
assertTrue(
String.format("Expected pattern %s, but was %s",
@@ -992,4 +1111,35 @@
return context;
}
+
+ private StepSegment getStepSegment(VibrationEffect effect, int index) {
+ VibrationEffectSegment segment = getEffectSegment(effect, index);
+ assertThat(segment).isInstanceOf(StepSegment.class);
+ return (StepSegment) segment;
+ }
+
+ private PrimitiveSegment getPrimitiveSegment(VibrationEffect effect, int index) {
+ VibrationEffectSegment segment = getEffectSegment(effect, index);
+ assertThat(segment).isInstanceOf(PrimitiveSegment.class);
+ return (PrimitiveSegment) segment;
+ }
+
+ private PrebakedSegment getPrebakedSegment(VibrationEffect effect, int index) {
+ VibrationEffectSegment segment = getEffectSegment(effect, index);
+ assertThat(segment).isInstanceOf(PrebakedSegment.class);
+ return (PrebakedSegment) segment;
+ }
+
+ private VibrationEffectSegment getEffectSegment(VibrationEffect effect, int index) {
+ assertThat(effect).isInstanceOf(VibrationEffect.Composed.class);
+ VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+ assertThat(index).isLessThan(composed.getSegments().size());
+ return composed.getSegments().get(index);
+ }
+
+ private PersistableBundle createNonEmptyBundle() {
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putInt("key", 1);
+ return bundle;
+ }
}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 9d4310c..363c1d8 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -148,7 +148,7 @@
"android.hardware.common-V2-java",
"android.hardware.light-V2.0-java",
"android.hardware.gnss-V2-java",
- "android.hardware.vibrator-V2-java",
+ "android.hardware.vibrator-V3-java",
"app-compat-annotations",
"framework-tethering.stubs.module_lib",
"keepanno-annotations",
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index bb2efa1..36a9c80 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1355,8 +1355,7 @@
int patternRepeatIndex = -1;
int amplitudeCount = -1;
- if (effect instanceof VibrationEffect.Composed) {
- VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+ if (effect instanceof VibrationEffect.Composed composed) {
int segmentCount = composed.getSegments().size();
pattern = new long[segmentCount];
amplitudes = new int[segmentCount];
@@ -1381,6 +1380,8 @@
}
pattern[amplitudeCount++] = segment.getDuration();
}
+ } else {
+ Slog.w(TAG, "Input devices don't support effect " + effect);
}
if (amplitudeCount < 0) {
diff --git a/services/core/java/com/android/server/vibrator/AbstractComposedVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractComposedVibratorStep.java
new file mode 100644
index 0000000..b263159
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/AbstractComposedVibratorStep.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.os.SystemClock;
+import android.os.VibrationEffect;
+
+import java.util.List;
+
+/**
+ * Represent a step on a single vibrator that plays one or more segments from a
+ * {@link VibrationEffect.Composed} effect.
+ */
+abstract class AbstractComposedVibratorStep extends AbstractVibratorStep {
+ public final VibrationEffect.Composed effect;
+ public final int segmentIndex;
+
+ /**
+ * @param conductor The {@link VibrationStepConductor} for these steps.
+ * @param startTime The time to schedule this step in the conductor.
+ * @param controller The vibrator that is playing the effect.
+ * @param effect The effect being played in this step.
+ * @param index The index of the next segment to be played by this step
+ * @param pendingVibratorOffDeadline The time the vibrator is expected to complete any
+ * previous vibration and turn off. This is used to allow this step to
+ * be triggered when the completion callback is received, and can
+ * be used to play effects back-to-back.
+ */
+ AbstractComposedVibratorStep(VibrationStepConductor conductor, long startTime,
+ VibratorController controller, VibrationEffect.Composed effect, int index,
+ long pendingVibratorOffDeadline) {
+ super(conductor, startTime, controller, pendingVibratorOffDeadline);
+ this.effect = effect;
+ this.segmentIndex = index;
+ }
+
+ /**
+ * Return the {@link VibrationStepConductor#nextVibrateStep} with start and off timings
+ * calculated from {@link #getVibratorOnDuration()} based on the current
+ * {@link SystemClock#uptimeMillis()} and jumping all played segments from the effect.
+ */
+ protected List<Step> nextSteps(int segmentsPlayed) {
+ // Schedule next steps to run right away.
+ long nextStartTime = SystemClock.uptimeMillis();
+ if (mVibratorOnResult > 0) {
+ // Vibrator was turned on by this step, with mVibratorOnResult as the duration.
+ // Schedule next steps for right after the vibration finishes.
+ nextStartTime += mVibratorOnResult;
+ }
+ return nextSteps(nextStartTime, segmentsPlayed);
+ }
+
+ /**
+ * Return the {@link VibrationStepConductor#nextVibrateStep} with given start time,
+ * which might be calculated independently, and jumping all played segments from the effect.
+ *
+ * <p>This should be used when the vibrator on/off state is not responsible for the step
+ * execution timing, e.g. while playing the vibrator amplitudes.
+ */
+ protected List<Step> nextSteps(long nextStartTime, int segmentsPlayed) {
+ int nextSegmentIndex = segmentIndex + segmentsPlayed;
+ int effectSize = effect.getSegments().size();
+ int repeatIndex = effect.getRepeatIndex();
+ if (nextSegmentIndex >= effectSize && repeatIndex >= 0) {
+ // Count the loops that were played.
+ int loopSize = effectSize - repeatIndex;
+ int loopSegmentsPlayed = nextSegmentIndex - repeatIndex;
+ getVibration().stats.reportRepetition(loopSegmentsPlayed / loopSize);
+ nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize);
+ }
+ Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect,
+ nextSegmentIndex, mPendingVibratorOffDeadline);
+ return List.of(nextStep);
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
index 90b6f95..42203b1 100644
--- a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
@@ -16,21 +16,16 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.SystemClock;
-import android.os.VibrationEffect;
import android.util.Slog;
import java.util.Arrays;
import java.util.List;
-/**
- * Represent a step on a single vibrator that plays one or more segments from a
- * {@link VibrationEffect.Composed} effect.
- */
+/** Represent a step on a single vibrator that plays a command on {@link VibratorController}. */
abstract class AbstractVibratorStep extends Step {
public final VibratorController controller;
- public final VibrationEffect.Composed effect;
- public final int segmentIndex;
long mVibratorOnResult;
long mPendingVibratorOffDeadline;
@@ -41,20 +36,15 @@
* @param startTime The time to schedule this step in the
* {@link VibrationStepConductor}.
* @param controller The vibrator that is playing the effect.
- * @param effect The effect being played in this step.
- * @param index The index of the next segment to be played by this step
* @param pendingVibratorOffDeadline The time the vibrator is expected to complete any
* previous vibration and turn off. This is used to allow this step to
* be triggered when the completion callback is received, and can
* be used to play effects back-to-back.
*/
AbstractVibratorStep(VibrationStepConductor conductor, long startTime,
- VibratorController controller, VibrationEffect.Composed effect, int index,
- long pendingVibratorOffDeadline) {
+ VibratorController controller, long pendingVibratorOffDeadline) {
super(conductor, startTime);
this.controller = controller;
- this.effect = effect;
- this.segmentIndex = index;
mPendingVibratorOffDeadline = pendingVibratorOffDeadline;
}
@@ -88,6 +78,7 @@
return shouldAcceptCallback;
}
+ @NonNull
@Override
public List<Step> cancel() {
return Arrays.asList(new CompleteEffectVibratorStep(conductor, SystemClock.uptimeMillis(),
@@ -138,43 +129,4 @@
controller.setAmplitude(amplitude);
getVibration().stats.reportSetAmplitude();
}
-
- /**
- * Return the {@link VibrationStepConductor#nextVibrateStep} with start and off timings
- * calculated from {@link #getVibratorOnDuration()} based on the current
- * {@link SystemClock#uptimeMillis()} and jumping all played segments from the effect.
- */
- protected List<Step> nextSteps(int segmentsPlayed) {
- // Schedule next steps to run right away.
- long nextStartTime = SystemClock.uptimeMillis();
- if (mVibratorOnResult > 0) {
- // Vibrator was turned on by this step, with mVibratorOnResult as the duration.
- // Schedule next steps for right after the vibration finishes.
- nextStartTime += mVibratorOnResult;
- }
- return nextSteps(nextStartTime, segmentsPlayed);
- }
-
- /**
- * Return the {@link VibrationStepConductor#nextVibrateStep} with given start time,
- * which might be calculated independently, and jumping all played segments from the effect.
- *
- * <p>This should be used when the vibrator on/off state is not responsible for the step
- * execution timing, e.g. while playing the vibrator amplitudes.
- */
- protected List<Step> nextSteps(long nextStartTime, int segmentsPlayed) {
- int nextSegmentIndex = segmentIndex + segmentsPlayed;
- int effectSize = effect.getSegments().size();
- int repeatIndex = effect.getRepeatIndex();
- if (nextSegmentIndex >= effectSize && repeatIndex >= 0) {
- // Count the loops that were played.
- int loopSize = effectSize - repeatIndex;
- int loopSegmentsPlayed = nextSegmentIndex - repeatIndex;
- getVibration().stats.reportRepetition(loopSegmentsPlayed / loopSize);
- nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize);
- }
- Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect,
- nextSegmentIndex, mPendingVibratorOffDeadline);
- return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep);
- }
}
diff --git a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
index 48dd992..7f9c349 100644
--- a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationEffect;
@@ -35,8 +36,7 @@
CompleteEffectVibratorStep(VibrationStepConductor conductor, long startTime, boolean cancelled,
VibratorController controller, long pendingVibratorOffDeadline) {
- super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
- pendingVibratorOffDeadline);
+ super(conductor, startTime, controller, pendingVibratorOffDeadline);
mCancelled = cancelled;
}
@@ -47,6 +47,7 @@
return mCancelled;
}
+ @NonNull
@Override
public List<Step> cancel() {
if (mCancelled) {
@@ -57,6 +58,7 @@
return super.cancel();
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "CompleteEffectVibratorStep");
diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
index 940bd08..e495af5 100644
--- a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.PrimitiveSegment;
@@ -31,7 +32,7 @@
* <p>This step will use the maximum supported number of consecutive segments of type
* {@link PrimitiveSegment} starting at the current index.
*/
-final class ComposePrimitivesVibratorStep extends AbstractVibratorStep {
+final class ComposePrimitivesVibratorStep extends AbstractComposedVibratorStep {
/**
* Default limit to the number of primitives in a composition, if none is defined by the HAL,
* to prevent repeating effects from generating an infinite list.
@@ -47,6 +48,7 @@
index, pendingVibratorOffDeadline);
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep");
diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
index 5d572be6..e8952fa 100644
--- a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.RampSegment;
@@ -31,7 +32,7 @@
* <p>This step will use the maximum supported number of consecutive segments of type
* {@link RampSegment}, starting at the current index.
*/
-final class ComposePwleVibratorStep extends AbstractVibratorStep {
+final class ComposePwleVibratorStep extends AbstractComposedVibratorStep {
/**
* Default limit to the number of PWLE segments, if none is defined by the HAL, to prevent
* repeating effects from generating an infinite list.
@@ -47,6 +48,7 @@
index, pendingVibratorOffDeadline);
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep");
diff --git a/services/core/java/com/android/server/vibrator/DeviceAdapter.java b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
index 98309cd..bd4fc07 100644
--- a/services/core/java/com/android/server/vibrator/DeviceAdapter.java
+++ b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
@@ -21,7 +21,6 @@
import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.VibrationEffectSegment;
-import android.util.Slog;
import android.util.SparseArray;
import java.util.ArrayList;
@@ -82,9 +81,8 @@
@NonNull
@Override
public VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect) {
- if (!(effect instanceof VibrationEffect.Composed)) {
+ if (!(effect instanceof VibrationEffect.Composed composed)) {
// Segments adapters can only apply to Composed effects.
- Slog.wtf(TAG, "Error adapting unsupported vibration effect: " + effect);
return effect;
}
@@ -95,7 +93,6 @@
}
VibratorInfo info = controller.getVibratorInfo();
- VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
List<VibrationEffectSegment> newSegments = new ArrayList<>(composed.getSegments());
int newRepeatIndex = composed.getRepeatIndex();
diff --git a/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java
index c9683d9..6456371 100644
--- a/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java
+++ b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.Trace;
import android.util.Slog;
@@ -43,6 +44,7 @@
return true;
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "FinishSequentialEffectStep");
@@ -61,6 +63,7 @@
}
}
+ @NonNull
@Override
public List<Step> cancel() {
cancelImmediately();
diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
index 8094e7c5..4b23216 100644
--- a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.PrebakedSegment;
@@ -31,7 +32,7 @@
* <p>This step automatically falls back by replacing the prebaked segment with
* {@link VibrationSettings#getFallbackEffect(int)}, if available.
*/
-final class PerformPrebakedVibratorStep extends AbstractVibratorStep {
+final class PerformPrebakedVibratorStep extends AbstractComposedVibratorStep {
PerformPrebakedVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller, VibrationEffect.Composed effect, int index,
@@ -42,6 +43,7 @@
index, pendingVibratorOffDeadline);
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformPrebakedVibratorStep");
diff --git a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java
new file mode 100644
index 0000000..8f36118
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.annotation.NonNull;
+import android.os.Trace;
+import android.os.VibrationEffect;
+
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator on with a vendor-specific vibration from a
+ * {@link VibrationEffect.VendorEffect} effect.
+ */
+final class PerformVendorEffectVibratorStep extends AbstractVibratorStep {
+ /**
+ * Timeout to ensure vendor vibrations are not unbounded if vibrator callbacks are lost.
+ */
+ static final long VENDOR_EFFECT_MAX_DURATION_MS = 60_000; // 1 min
+
+ public final VibrationEffect.VendorEffect effect;
+
+ PerformVendorEffectVibratorStep(VibrationStepConductor conductor, long startTime,
+ VibratorController controller, VibrationEffect.VendorEffect effect,
+ long pendingVibratorOffDeadline) {
+ // This step should wait for the last vibration to finish (with the timeout) and for the
+ // intended step start time (to respect the effect delays).
+ super(conductor, Math.max(startTime, pendingVibratorOffDeadline), controller,
+ pendingVibratorOffDeadline);
+ this.effect = effect;
+ }
+
+ @NonNull
+ @Override
+ public List<Step> play() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformVendorEffectVibratorStep");
+ try {
+ long vibratorOnResult = controller.on(effect, getVibration().id);
+ vibratorOnResult = Math.min(vibratorOnResult, VENDOR_EFFECT_MAX_DURATION_MS);
+ handleVibratorOnResult(vibratorOnResult);
+ return List.of(new CompleteEffectVibratorStep(conductor, startTime,
+ /* cancelled= */ false, controller, mPendingVibratorOffDeadline));
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
index f40c994..901f9c3 100644
--- a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.SystemClock;
import android.os.Trace;
import android.util.Slog;
@@ -31,8 +32,7 @@
RampOffVibratorStep(VibrationStepConductor conductor, long startTime, float amplitudeTarget,
float amplitudeDelta, VibratorController controller,
long pendingVibratorOffDeadline) {
- super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
- pendingVibratorOffDeadline);
+ super(conductor, startTime, controller, pendingVibratorOffDeadline);
mAmplitudeTarget = amplitudeTarget;
mAmplitudeDelta = amplitudeDelta;
}
@@ -42,12 +42,14 @@
return true;
}
+ @NonNull
@Override
public List<Step> cancel() {
return Arrays.asList(new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(),
controller, /* isCleanUp= */ true));
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffVibratorStep");
diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
index e13ec6c..8478e77 100644
--- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationEffect;
@@ -32,7 +33,7 @@
* <p>This step ignores vibration completion callbacks and control the vibrator on/off state
* and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}.
*/
-final class SetAmplitudeVibratorStep extends AbstractVibratorStep {
+final class SetAmplitudeVibratorStep extends AbstractComposedVibratorStep {
/**
* The repeating waveform keeps the vibrator ON all the time. Use a minimum duration to
* prevent short patterns from turning the vibrator ON too frequently.
@@ -69,6 +70,7 @@
return shouldAcceptCallback;
}
+ @NonNull
@Override
public List<Step> play() {
// TODO: consider separating the "on" steps at the start into a separate Step.
diff --git a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java
index c197271..3ceba57 100644
--- a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java
+++ b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.vibrator.IVibratorManager;
import android.os.CombinedVibration;
@@ -74,6 +75,7 @@
return mVibratorsOnMaxDuration;
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartSequentialEffectStep");
@@ -111,6 +113,7 @@
return nextSteps;
}
+ @NonNull
@Override
public List<Step> cancel() {
return VibrationStepConductor.EMPTY_STEP_LIST;
@@ -173,13 +176,12 @@
for (int i = 0; i < vibratorCount; i++) {
steps[i] = conductor.nextVibrateStep(vibrationStartTime,
conductor.getVibrators().get(effectMapping.vibratorIdAt(i)),
- effectMapping.effectAt(i),
- /* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0);
+ effectMapping.effectAt(i));
}
if (steps.length == 1) {
// No need to prepare and trigger sync effects on a single vibrator.
- return startVibrating(steps[0], nextSteps);
+ return startVibrating(steps[0], effectMapping.effectAt(0), nextSteps);
}
// This synchronization of vibrators should be executed one at a time, even if we are
@@ -196,8 +198,8 @@
effectMapping.getRequiredSyncCapabilities(),
effectMapping.getVibratorIds());
- for (AbstractVibratorStep step : steps) {
- long duration = startVibrating(step, nextSteps);
+ for (int i = 0; i < vibratorCount; i++) {
+ long duration = startVibrating(steps[i], effectMapping.effectAt(i), nextSteps);
if (duration < 0) {
// One vibrator has failed, fail this entire sync attempt.
hasFailed = true;
@@ -231,7 +233,12 @@
return hasFailed ? -1 : maxDuration;
}
- private long startVibrating(AbstractVibratorStep step, List<Step> nextSteps) {
+ private long startVibrating(@Nullable AbstractVibratorStep step, VibrationEffect effect,
+ List<Step> nextSteps) {
+ if (step == null) {
+ // Failed to create a step for VibrationEffect.
+ return -1;
+ }
nextSteps.addAll(step.play());
long stepDuration = step.getVibratorOnDuration();
if (stepDuration < 0) {
@@ -239,7 +246,7 @@
return stepDuration;
}
// Return the longest estimation for the entire effect.
- return Math.max(stepDuration, step.effect.getDuration());
+ return Math.max(stepDuration, effect.getDuration());
}
/**
@@ -249,28 +256,20 @@
* play all of the effects in sync.
*/
final class DeviceEffectMap {
- private final SparseArray<VibrationEffect.Composed> mVibratorEffects;
+ private final SparseArray<VibrationEffect> mVibratorEffects;
private final int[] mVibratorIds;
private final long mRequiredSyncCapabilities;
DeviceEffectMap(CombinedVibration.Mono mono) {
SparseArray<VibratorController> vibrators = conductor.getVibrators();
VibrationEffect effect = mono.getEffect();
- if (effect instanceof VibrationEffect.Composed) {
- mVibratorEffects = new SparseArray<>(vibrators.size());
- mVibratorIds = new int[vibrators.size()];
+ mVibratorEffects = new SparseArray<>(vibrators.size());
+ mVibratorIds = new int[vibrators.size()];
- VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect;
- for (int i = 0; i < vibrators.size(); i++) {
- int vibratorId = vibrators.keyAt(i);
- mVibratorEffects.put(vibratorId, composedEffect);
- mVibratorIds[i] = vibratorId;
- }
- } else {
- Slog.wtf(VibrationThread.TAG,
- "Unable to map device vibrators to unexpected effect: " + effect);
- mVibratorEffects = new SparseArray<>();
- mVibratorIds = new int[0];
+ for (int i = 0; i < vibrators.size(); i++) {
+ int vibratorId = vibrators.keyAt(i);
+ mVibratorEffects.put(vibratorId, effect);
+ mVibratorIds[i] = vibratorId;
}
mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects);
}
@@ -282,13 +281,7 @@
for (int i = 0; i < stereoEffects.size(); i++) {
int vibratorId = stereoEffects.keyAt(i);
if (vibrators.contains(vibratorId)) {
- VibrationEffect effect = stereoEffects.valueAt(i);
- if (effect instanceof VibrationEffect.Composed) {
- mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect);
- } else {
- Slog.wtf(VibrationThread.TAG,
- "Unable to map device vibrators to unexpected effect: " + effect);
- }
+ mVibratorEffects.put(vibratorId, stereoEffects.valueAt(i));
}
}
mVibratorIds = new int[mVibratorEffects.size()];
@@ -326,7 +319,7 @@
}
/** Return the {@link VibrationEffect} at given index. */
- public VibrationEffect.Composed effectAt(int index) {
+ public VibrationEffect effectAt(int index) {
return mVibratorEffects.valueAt(index);
}
@@ -338,16 +331,24 @@
* IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities.
*/
private long calculateRequiredSyncCapabilities(
- SparseArray<VibrationEffect.Composed> effects) {
+ SparseArray<VibrationEffect> effects) {
long prepareCap = 0;
for (int i = 0; i < effects.size(); i++) {
- VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0);
- if (firstSegment instanceof StepSegment) {
- prepareCap |= IVibratorManager.CAP_PREPARE_ON;
- } else if (firstSegment instanceof PrebakedSegment) {
+ VibrationEffect effect = effects.valueAt(i);
+ if (effect instanceof VibrationEffect.VendorEffect) {
prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM;
- } else if (firstSegment instanceof PrimitiveSegment) {
- prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE;
+ } else if (effect instanceof VibrationEffect.Composed composed) {
+ VibrationEffectSegment firstSegment = composed.getSegments().get(0);
+ if (firstSegment instanceof StepSegment) {
+ prepareCap |= IVibratorManager.CAP_PREPARE_ON;
+ } else if (firstSegment instanceof PrebakedSegment) {
+ prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM;
+ } else if (firstSegment instanceof PrimitiveSegment) {
+ prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE;
+ }
+ } else {
+ Slog.wtf(VibrationThread.TAG,
+ "Unable to check sync capabilities to unexpected effect: " + effect);
}
}
int triggerCap = 0;
diff --git a/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java
index 065ce11..87dc269 100644
--- a/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.os.SystemClock;
import android.os.Trace;
@@ -36,7 +37,7 @@
TurnOffVibratorStep(VibrationStepConductor conductor, long startTime,
VibratorController controller, boolean isCleanUp) {
- super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, startTime);
+ super(conductor, startTime, controller, startTime);
mIsCleanUp = isCleanUp;
}
@@ -45,6 +46,7 @@
return mIsCleanUp;
}
+ @NonNull
@Override
public List<Step> cancel() {
return Arrays.asList(new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(),
@@ -56,6 +58,7 @@
stopVibrating();
}
+ @NonNull
@Override
public List<Step> play() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "TurnOffVibratorStep");
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 6537228..584fac8 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -398,13 +398,14 @@
private void dumpEffect(
ProtoOutputStream proto, long fieldId, VibrationEffect effect) {
- final long token = proto.start(fieldId);
- VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
- for (VibrationEffectSegment segment : composed.getSegments()) {
- dumpEffect(proto, VibrationEffectProto.SEGMENTS, segment);
+ if (effect instanceof VibrationEffect.Composed composed) {
+ final long token = proto.start(fieldId);
+ for (VibrationEffectSegment segment : composed.getSegments()) {
+ dumpEffect(proto, VibrationEffectProto.SEGMENTS, segment);
+ }
+ proto.write(VibrationEffectProto.REPEAT, composed.getRepeatIndex());
+ proto.end(token);
}
- proto.write(VibrationEffectProto.REPEAT, composed.getRepeatIndex());
- proto.end(token);
}
private void dumpEffect(ProtoOutputStream proto, long fieldId,
diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index d9ca710..3933759 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -25,14 +25,12 @@
import android.os.Vibrator;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
-import android.os.vibrator.VibrationEffectSegment;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import java.io.PrintWriter;
-import java.util.ArrayList;
import java.util.Locale;
/** Controls vibration scaling. */
@@ -136,12 +134,6 @@
*/
@NonNull
public VibrationEffect scale(@NonNull VibrationEffect effect, int usageHint) {
- if (!(effect instanceof VibrationEffect.Composed)) {
- // This only scales composed vibration effects.
- Slog.wtf(TAG, "Error scaling unsupported vibration effect: " + effect);
- return effect;
- }
-
int newEffectStrength = getEffectStrength(usageHint);
ScaleLevel scaleLevel = mScaleLevels.get(getScaleLevel(usageHint));
float adaptiveScale = getAdaptiveHapticsScale(usageHint);
@@ -154,26 +146,10 @@
scaleLevel = SCALE_LEVEL_NONE;
}
- VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect;
- ArrayList<VibrationEffectSegment> segments =
- new ArrayList<>(composedEffect.getSegments());
- int segmentCount = segments.size();
- for (int i = 0; i < segmentCount; i++) {
- segments.set(i,
- segments.get(i).resolve(mDefaultVibrationAmplitude)
- .applyEffectStrength(newEffectStrength)
- .scale(scaleLevel.factor)
- .scaleLinearly(adaptiveScale));
- }
- if (segments.equals(composedEffect.getSegments())) {
- // No segment was updated, return original effect.
- return effect;
- }
- VibrationEffect.Composed scaled =
- new VibrationEffect.Composed(segments, composedEffect.getRepeatIndex());
- // Make sure we validate what was scaled, since we're using the constructor directly
- scaled.validate();
- return scaled;
+ return effect.resolve(mDefaultVibrationAmplitude)
+ .applyEffectStrength(newEffectStrength)
+ .scale(scaleLevel.factor)
+ .scaleLinearly(adaptiveScale);
}
/**
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index f3e226e..8c9a92d 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -123,6 +123,24 @@
@Nullable
AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller,
+ VibrationEffect effect) {
+ if (Build.IS_DEBUGGABLE) {
+ expectIsVibrationThread(true);
+ }
+ if (effect instanceof VibrationEffect.VendorEffect vendorEffect) {
+ return new PerformVendorEffectVibratorStep(this, startTime, controller, vendorEffect,
+ /* pendingVibratorOffDeadline= */ 0);
+ }
+ if (effect instanceof VibrationEffect.Composed composed) {
+ return nextVibrateStep(startTime, controller, composed, /* segmentIndex= */ 0,
+ /* pendingVibratorOffDeadline= */ 0);
+ }
+ Slog.wtf(TAG, "Unable to create next step for unexpected effect: " + effect);
+ return null;
+ }
+
+ @NonNull
+ AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller,
VibrationEffect.Composed effect, int segmentIndex, long pendingVibratorOffDeadline) {
if (Build.IS_DEBUGGABLE) {
expectIsVibrationThread(true);
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 988e8fe..8cc157c 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -20,8 +20,10 @@
import android.hardware.vibrator.IVibrator;
import android.os.Binder;
import android.os.IVibratorStateListener;
+import android.os.Parcel;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
+import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
@@ -262,6 +264,35 @@
}
/**
+ * Plays vendor vibration effect, using {@code vibrationId} for completion callback to
+ * {@link OnVibrationCompleteListener}.
+ *
+ * <p>This will affect the state of {@link #isVibrating()}.
+ *
+ * @return The positive duration of the vibration started, if successful, zero if the vibrator
+ * do not support the input or a negative number if the operation failed.
+ */
+ public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) {
+ synchronized (mLock) {
+ Parcel vendorData = Parcel.obtain();
+ try {
+ vendorEffect.getVendorData().writeToParcel(vendorData, /* flags= */ 0);
+ vendorData.setDataPosition(0);
+ long duration = mNativeWrapper.performVendorEffect(vendorData,
+ vendorEffect.getEffectStrength(), vendorEffect.getLinearScale(),
+ vibrationId);
+ if (duration > 0) {
+ mCurrentAmplitude = -1;
+ notifyListenerOnVibrating(true);
+ }
+ return duration;
+ } finally {
+ vendorData.recycle();
+ }
+ }
+ }
+
+ /**
* Plays predefined vibration effect, using {@code vibrationId} for completion callback to
* {@link OnVibrationCompleteListener}.
*
@@ -427,6 +458,9 @@
private static native long performEffect(long nativePtr, long effect, long strength,
long vibrationId);
+ private static native long performVendorEffect(long nativePtr, Parcel vendorData,
+ long strength, float scale, long vibrationId);
+
private static native long performComposedEffect(long nativePtr, PrimitiveSegment[] effect,
long vibrationId);
@@ -482,6 +516,12 @@
return performEffect(mNativePtr, effect, strength, vibrationId);
}
+ /** Turns vibrator on to perform a vendor-specific effect. */
+ public long performVendorEffect(Parcel vendorData, long strength, float scale,
+ long vibrationId) {
+ return performVendorEffect(mNativePtr, vendorData, strength, scale, vibrationId);
+ }
+
/** Turns vibrator on to perform effect composed of give primitives effect. */
public long compose(PrimitiveSegment[] primitives, long vibrationId) {
return performComposedEffect(mNativePtr, primitives, vibrationId);
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 4437a2d..7d1d5c9 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -521,6 +521,11 @@
Slog.e(TAG, "token must not be null");
return null;
}
+ if (effect.hasVendorEffects()
+ && !hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) {
+ Slog.w(TAG, "vibrate; no permission for vendor effects");
+ return null;
+ }
enforceUpdateAppOpsStatsPermission(uid);
if (!isEffectValid(effect)) {
return null;
@@ -1285,12 +1290,13 @@
}
private void fillVibrationFallbacks(HalVibration vib, VibrationEffect effect) {
- VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+ if (!(effect instanceof VibrationEffect.Composed composed)) {
+ return;
+ }
int segmentCount = composed.getSegments().size();
for (int i = 0; i < segmentCount; i++) {
VibrationEffectSegment segment = composed.getSegments().get(i);
- if (segment instanceof PrebakedSegment) {
- PrebakedSegment prebaked = (PrebakedSegment) segment;
+ if (segment instanceof PrebakedSegment prebaked) {
VibrationEffect fallback = mVibrationSettings.getFallbackEffect(
prebaked.getEffectId());
if (prebaked.shouldFallback() && fallback != null) {
@@ -1373,12 +1379,11 @@
@Nullable
private static PrebakedSegment extractPrebakedSegment(VibrationEffect effect) {
- if (effect instanceof VibrationEffect.Composed) {
- VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+ if (effect instanceof VibrationEffect.Composed composed) {
if (composed.getSegments().size() == 1) {
VibrationEffectSegment segment = composed.getSegments().get(0);
- if (segment instanceof PrebakedSegment) {
- return (PrebakedSegment) segment;
+ if (segment instanceof PrebakedSegment prebaked) {
+ return prebaked;
}
}
}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 3cd5f76..9fa1a53 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -193,7 +193,7 @@
"android.hardware.thermal-V2-ndk",
"android.hardware.tv.input@1.0",
"android.hardware.tv.input-V2-ndk",
- "android.hardware.vibrator-V2-ndk",
+ "android.hardware.vibrator-V3-ndk",
"android.hardware.vibrator@1.0",
"android.hardware.vibrator@1.1",
"android.hardware.vibrator@1.2",
diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
index 2804a10..f12930a 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
@@ -17,7 +17,10 @@
#define LOG_TAG "VibratorController"
#include <aidl/android/hardware/vibrator/IVibrator.h>
+#include <android/binder_parcel.h>
+#include <android/binder_parcel_jni.h>
#include <android/hardware/vibrator/1.3/IVibrator.h>
+#include <android/persistable_bundle_aidl.h>
#include <nativehelper/JNIHelp.h>
#include <utils/Log.h>
#include <utils/misc.h>
@@ -32,6 +35,8 @@
namespace V1_3 = android::hardware::vibrator::V1_3;
namespace Aidl = aidl::android::hardware::vibrator;
+using aidl::android::os::PersistableBundle;
+
namespace android {
static JavaVM* sJvm = nullptr;
@@ -95,7 +100,7 @@
return nullptr;
}
auto result = manager->getVibrator(vibratorId);
- return result.isOk() ? std::move(result.value()) : nullptr;
+ return result.isOk() ? result.value() : nullptr;
}
class VibratorControllerWrapper {
@@ -192,6 +197,29 @@
return effect;
}
+static Aidl::VendorEffect vendorEffectFromJavaParcel(JNIEnv* env, jobject vendorData,
+ jlong strength, jfloat scale) {
+ PersistableBundle bundle;
+ if (AParcel* parcel = AParcel_fromJavaParcel(env, vendorData); parcel != nullptr) {
+ if (binder_status_t status = bundle.readFromParcel(parcel); status == STATUS_OK) {
+ AParcel_delete(parcel);
+ } else {
+ jniThrowExceptionFmt(env, "android/os/BadParcelableException",
+ "Failed to readFromParcel, status %d (%s)", status,
+ strerror(-status));
+ }
+ } else {
+ jniThrowExceptionFmt(env, "android/os/BadParcelableException",
+ "Failed to AParcel_fromJavaParcel, for nullptr");
+ }
+
+ Aidl::VendorEffect effect;
+ effect.vendorData = bundle;
+ effect.strength = static_cast<Aidl::EffectStrength>(strength);
+ effect.scale = static_cast<float>(scale);
+ return effect;
+}
+
static void destroyNativeWrapper(void* ptr) {
VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
if (wrapper) {
@@ -289,6 +317,23 @@
return result.isOk() ? result.value().count() : (result.isUnsupported() ? 0 : -1);
}
+static jlong vibratorPerformVendorEffect(JNIEnv* env, jclass /* clazz */, jlong ptr,
+ jobject vendorData, jlong strength, jfloat scale,
+ jlong vibrationId) {
+ VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
+ if (wrapper == nullptr) {
+ ALOGE("vibratorPerformVendorEffect failed because native wrapper was not initialized");
+ return -1;
+ }
+ Aidl::VendorEffect effect = vendorEffectFromJavaParcel(env, vendorData, strength, scale);
+ auto callback = wrapper->createCallback(vibrationId);
+ auto performVendorEffectFn = [&effect, &callback](vibrator::HalWrapper* hal) {
+ return hal->performVendorEffect(effect, callback);
+ };
+ auto result = wrapper->halCall<void>(performVendorEffectFn, "performVendorEffect");
+ return result.isOk() ? std::numeric_limits<int64_t>::max() : (result.isUnsupported() ? 0 : -1);
+}
+
static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlong ptr,
jobjectArray composition, jlong vibrationId) {
VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
@@ -466,6 +511,7 @@
{"off", "(J)V", (void*)vibratorOff},
{"setAmplitude", "(JF)V", (void*)vibratorSetAmplitude},
{"performEffect", "(JJJJ)J", (void*)vibratorPerformEffect},
+ {"performVendorEffect", "(JLandroid/os/Parcel;JFJ)J", (void*)vibratorPerformVendorEffect},
{"performComposedEffect", "(J[Landroid/os/vibrator/PrimitiveSegment;J)J",
(void*)vibratorPerformComposedEffect},
{"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J",
diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp
index a738acb..598e273 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -63,7 +63,7 @@
libs: [
"android.hardware.power-V1-java",
"android.hardware.tv.cec-V1.0-java",
- "android.hardware.vibrator-V2-java",
+ "android.hardware.vibrator-V3-java",
"android.hidl.manager-V1.0-java",
"android.test.mock",
"android.test.base",
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index b9e99dd..a888dad 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -94,7 +94,7 @@
libs: [
"android.hardware.power-V1-java",
"android.hardware.tv.cec-V1.0-java",
- "android.hardware.vibrator-V2-java",
+ "android.hardware.vibrator-V3-java",
"android.hidl.manager-V1.0-java",
"android.test.mock",
"android.test.base",
diff --git a/services/tests/vibrator/Android.bp b/services/tests/vibrator/Android.bp
index da21cd3..757bcd8 100644
--- a/services/tests/vibrator/Android.bp
+++ b/services/tests/vibrator/Android.bp
@@ -16,7 +16,7 @@
],
libs: [
- "android.hardware.vibrator-V2-java",
+ "android.hardware.vibrator-V3-java",
"android.test.mock",
"android.test.base",
"android.test.runner",
@@ -36,7 +36,6 @@
"platform-test-annotations",
"service-permission.stubs.system_server",
"services.core",
- "flag-junit",
],
jni_libs: ["libdexmakerjvmtiagent"],
platform_apis: true,
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
index 3013ed0..59d5577 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
@@ -25,6 +25,7 @@
import android.hardware.vibrator.IVibrator;
import android.os.CombinedVibration;
import android.os.Handler;
+import android.os.PersistableBundle;
import android.os.VibrationEffect;
import android.os.test.TestLooper;
import android.os.vibrator.PrebakedSegment;
@@ -32,6 +33,7 @@
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
@@ -103,6 +105,17 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testVendorEffect_returnsOriginalSegment() {
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("key", 1);
+ VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData);
+
+ assertThat(mAdapter.adaptToVibrator(EMPTY_VIBRATOR_ID, effect)).isEqualTo(effect);
+ assertThat(mAdapter.adaptToVibrator(PWLE_VIBRATOR_ID, effect)).isEqualTo(effect);
+ }
+
+ @Test
public void testStepAndRampSegments_withoutPwleCapability_convertsRampsToSteps() {
VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
// Step(amplitude, frequencyHz, duration)
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
index b264435..9ebeaa8 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -37,6 +37,7 @@
import android.content.pm.PackageManagerInternal;
import android.os.ExternalVibrationScale;
import android.os.Handler;
+import android.os.PersistableBundle;
import android.os.PowerManagerInternal;
import android.os.UserHandle;
import android.os.VibrationAttributes;
@@ -232,6 +233,34 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void scale_withVendorEffect_setsEffectStrengthBasedOnSettings() {
+ setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_LOW);
+ setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putString("key", "value");
+ VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData);
+
+ VibrationEffect.VendorEffect scaled =
+ (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION);
+ assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+ setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
+ VIBRATION_INTENSITY_MEDIUM);
+ scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION);
+ assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_MEDIUM);
+
+ setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW);
+ scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION);
+ assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_LIGHT);
+
+ setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_OFF);
+ scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION);
+ // Vibration setting being bypassed will use default setting.
+ assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_LIGHT);
+ }
+
+ @Test
public void scale_withOneShotAndWaveform_resolvesAmplitude() {
// No scale, default amplitude still resolved
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_LOW);
@@ -365,6 +394,30 @@
assertTrue(scaled.getAmplitude() > 0.5);
}
+ @Test
+ @RequiresFlagsEnabled({
+ android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
+ android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS,
+ })
+ public void scale_adaptiveHapticsOnVendorEffect_setsLinearScaleParameter() {
+ setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
+
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("key", 1);
+ VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData);
+
+ VibrationEffect.VendorEffect scaled =
+ (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_RINGTONE);
+ assertEquals(scaled.getLinearScale(), 0.5f);
+
+ mVibrationScaler.removeAdaptiveHapticsScale(USAGE_RINGTONE);
+
+ scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_RINGTONE);
+ assertEquals(scaled.getLinearScale(), 1.0f);
+ }
+
private void setDefaultIntensity(@VibrationAttributes.Usage int usage,
@Vibrator.VibrationIntensity int intensity) {
when(mVibrationConfigMock.getDefaultVibrationIntensity(eq(usage))).thenReturn(intensity);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 9dac23f..02546d6 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -48,6 +48,7 @@
import android.os.CombinedVibration;
import android.os.Handler;
import android.os.IBinder;
+import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.Process;
import android.os.SystemClock;
@@ -560,8 +561,37 @@
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
Thread cancellingThread =
new Thread(() -> mVibrationConductor.notifyCancelled(
- new Vibration.EndInfo(
- Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE),
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE),
+ /* immediate= */ false));
+ cancellingThread.start();
+
+ waitForCompletion(/* timeout= */ 50);
+ cancellingThread.join();
+
+ verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE);
+ assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void vibrate_singleVibratorVendorEffectCancel_cancelsVibrationImmediately()
+ throws Exception {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+ // Set long vendor effect duration to check it gets cancelled quickly.
+ mVibratorProviders.get(VIBRATOR_ID).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS);
+
+ VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
+ long vibrationId = startThreadAndDispatcher(effect);
+
+ assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
+ TEST_TIMEOUT_MILLIS));
+ assertTrue(mThread.isRunningVibrationId(vibrationId));
+
+ // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
+ // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
+ Thread cancellingThread =
+ new Thread(() -> mVibrationConductor.notifyCancelled(
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE),
/* immediate= */ false));
cancellingThread.start();
@@ -588,8 +618,7 @@
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
Thread cancellingThread =
new Thread(() -> mVibrationConductor.notifyCancelled(
- new Vibration.EndInfo(
- Vibration.Status.CANCELLED_BY_SCREEN_OFF),
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF),
/* immediate= */ false));
cancellingThread.start();
@@ -654,6 +683,27 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void vibrate_singleVibratorVendorEffect_runsVibration() {
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+
+ VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
+ long vibrationId = startThreadAndDispatcher(effect);
+ waitForCompletion();
+
+ verify(mManagerHooks).noteVibratorOn(eq(UID),
+ eq(PerformVendorEffectVibratorStep.VENDOR_EFFECT_MAX_DURATION_MS));
+ verify(mManagerHooks).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
+ assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
+
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+ .containsExactly(effect)
+ .inOrder();
+ }
+
+ @Test
public void vibrate_singleVibratorComposed_runsVibration() {
FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
@@ -1437,16 +1487,48 @@
.combine();
long vibrationId = startThreadAndDispatcher(effect);
- assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(),
- TEST_TIMEOUT_MILLIS));
+ assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
assertTrue(mThread.isRunningVibrationId(vibrationId));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
Thread cancellingThread = new Thread(
() -> mVibrationConductor.notifyCancelled(
- new Vibration.EndInfo(
- Vibration.Status.CANCELLED_BY_SCREEN_OFF),
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF),
+ /* immediate= */ false));
+ cancellingThread.start();
+
+ waitForCompletion(/* timeout= */ 50);
+ cancellingThread.join();
+
+ verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_SCREEN_OFF);
+ assertFalse(mControllers.get(1).isVibrating());
+ assertFalse(mControllers.get(2).isVibrating());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void vibrate_multipleVendorEffectCancel_cancelsVibrationImmediately() throws Exception {
+ mockVibrators(1, 2);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+ mVibratorProviders.get(1).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS);
+ mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+ mVibratorProviders.get(2).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS);
+
+ CombinedVibration effect = CombinedVibration.startParallel()
+ .addVibrator(1, VibrationEffect.createVendorEffect(createTestVendorData()))
+ .addVibrator(2, VibrationEffect.createVendorEffect(createTestVendorData()))
+ .combine();
+ long vibrationId = startThreadAndDispatcher(effect);
+
+ assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
+ assertTrue(mThread.isRunningVibrationId(vibrationId));
+
+ // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
+ // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
+ Thread cancellingThread = new Thread(
+ () -> mVibrationConductor.notifyCancelled(
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF),
/* immediate= */ false));
cancellingThread.start();
@@ -1614,6 +1696,25 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void vibrate_vendorEffectWithRampDown_doesNotAddRampDown() {
+ when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15);
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+
+ VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
+ long vibrationId = startThreadAndDispatcher(effect);
+ waitForCompletion();
+
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
+
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+ .containsExactly(effect)
+ .inOrder();
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty();
+ }
+
+ @Test
public void vibrate_composedWithRampDown_doesNotAddRampDown() {
when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15);
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL,
@@ -1835,6 +1936,16 @@
return array;
}
+ private static PersistableBundle createTestVendorData() {
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("id", 1);
+ vendorData.putDouble("scale", 0.5);
+ vendorData.putBoolean("loop", false);
+ vendorData.putLongArray("amplitudes", new long[] { 0, 255, 128 });
+ vendorData.putString("label", "vibration");
+ return vendorData;
+ }
+
private VibrationEffectSegment expectedOneShot(long millis) {
return new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE,
/* frequencyHz= */ 0, (int) millis);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index ef944db..d5bcd53 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.vibrator;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -66,6 +68,7 @@
import android.os.IExternalVibrationController;
import android.os.IVibratorStateListener;
import android.os.Looper;
+import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
@@ -1545,6 +1548,50 @@
}
@Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void vibrate_vendorEffectsWithoutPermission_doesNotVibrate() throws Exception {
+ // Deny permission to vibrate with vendor effects
+ denyPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS);
+ mockVibrators(1);
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+ fakeVibrator.setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+ fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_TICK);
+ VibratorManagerService service = createSystemReadyService();
+
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putString("key", "value");
+ VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData);
+ VibrationEffect tickEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK);
+
+ vibrateAndWaitUntilFinished(service, vendorEffect, RINGTONE_ATTRS);
+ vibrateAndWaitUntilFinished(service, tickEffect, RINGTONE_ATTRS);
+
+ // No vendor effect played, but predefined TICK plays successfully.
+ assertThat(fakeVibrator.getAllVendorEffects()).isEmpty();
+ assertThat(fakeVibrator.getAllEffectSegments()).hasSize(1);
+ assertThat(fakeVibrator.getAllEffectSegments().get(0)).isInstanceOf(PrebakedSegment.class);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void vibrate_vendorEffectsWithPermission_successful() throws Exception {
+ // Deny permission to vibrate with vendor effects
+ grantPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS);
+ mockVibrators(1);
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+ fakeVibrator.setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+ VibratorManagerService service = createSystemReadyService();
+
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putString("key", "value");
+ VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData);
+
+ vibrateAndWaitUntilFinished(service, vendorEffect, RINGTONE_ATTRS);
+
+ assertThat(fakeVibrator.getAllVendorEffects()).containsExactly(vendorEffect);
+ }
+
+ @Test
public void vibrate_withIntensitySettings_appliesSettingsToScaleVibrations() throws Exception {
int defaultNotificationIntensity =
mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_NOTIFICATION);
@@ -1686,6 +1733,39 @@
}
@Test
+ @RequiresFlagsEnabled({
+ android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
+ android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS,
+ })
+ public void vibrate_withIntensitySettingsAndAdaptiveHaptics_appliesSettingsToVendorEffects()
+ throws Exception {
+ setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
+ Vibrator.VIBRATION_INTENSITY_LOW);
+
+ mockVibrators(1);
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+ fakeVibrator.setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+ VibratorManagerService service = createSystemReadyService();
+
+ SparseArray<Float> vibrationScales = new SparseArray<>();
+ vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
+
+ mVibratorControlService.setVibrationParams(
+ VibrationParamGenerator.generateVibrationParams(vibrationScales),
+ mFakeVibratorController);
+
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putString("key", "value");
+ VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData);
+ vibrateAndWaitUntilFinished(service, vendorEffect, NOTIFICATION_ATTRS);
+
+ assertThat(fakeVibrator.getAllVendorEffects()).hasSize(1);
+ VibrationEffect.VendorEffect scaled = fakeVibrator.getAllVendorEffects().get(0);
+ assertThat(scaled.getEffectStrength()).isEqualTo(VibrationEffect.EFFECT_STRENGTH_STRONG);
+ assertThat(scaled.getLinearScale()).isEqualTo(0.4f);
+ }
+
+ @Test
public void vibrate_withPowerModeChange_cancelVibrationIfNotAllowed() throws Exception {
mockVibrators(1, 2);
VibratorManagerService service = createSystemReadyService();
@@ -2701,7 +2781,9 @@
CombinedVibration effect, VibrationAttributes attrs) {
HalVibration vib = service.vibrateWithPermissionCheck(UID, deviceId, PACKAGE_NAME, effect,
attrs, "some reason", service);
- mPendingVibrations.add(vib);
+ if (vib != null) {
+ mPendingVibrations.add(vib);
+ }
return vib;
}
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
index 2ddb47b..96c3e97 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -17,8 +17,11 @@
package com.android.server.vibrator;
import android.annotation.Nullable;
+import android.hardware.vibrator.IVibrator;
import android.os.Handler;
import android.os.Looper;
+import android.os.Parcel;
+import android.os.PersistableBundle;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
@@ -45,6 +48,7 @@
private final Map<Long, PrebakedSegment> mEnabledAlwaysOnEffects = new HashMap<>();
private final Map<Long, List<VibrationEffectSegment>> mEffectSegments = new TreeMap<>();
+ private final Map<Long, List<VibrationEffect.VendorEffect>> mVendorEffects = new TreeMap<>();
private final Map<Long, List<Integer>> mBraking = new HashMap<>();
private final List<Float> mAmplitudes = new ArrayList<>();
private final List<Boolean> mExternalControlStates = new ArrayList<>();
@@ -69,11 +73,16 @@
private float mFrequencyResolution = Float.NaN;
private float mQFactor = Float.NaN;
private float[] mMaxAmplitudes;
+ private long mVendorEffectDuration = EFFECT_DURATION;
void recordEffectSegment(long vibrationId, VibrationEffectSegment segment) {
mEffectSegments.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(segment);
}
+ void recordVendorEffect(long vibrationId, VibrationEffect.VendorEffect vendorEffect) {
+ mVendorEffects.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(vendorEffect);
+ }
+
void recordBraking(long vibrationId, int braking) {
mBraking.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(braking);
}
@@ -130,6 +139,21 @@
}
@Override
+ public long performVendorEffect(Parcel vendorData, long strength, float scale,
+ long vibrationId) {
+ if ((mCapabilities & IVibrator.CAP_PERFORM_VENDOR_EFFECTS) == 0) {
+ return 0;
+ }
+ PersistableBundle bundle = PersistableBundle.CREATOR.createFromParcel(vendorData);
+ recordVendorEffect(vibrationId,
+ new VibrationEffect.VendorEffect(bundle, (int) strength, scale));
+ applyLatency(mOnLatency);
+ scheduleListener(mVendorEffectDuration, vibrationId);
+ // HAL has unknown duration for vendor effects.
+ return Long.MAX_VALUE;
+ }
+
+ @Override
public long compose(PrimitiveSegment[] primitives, long vibrationId) {
if (mSupportedPrimitives == null) {
return 0;
@@ -328,6 +352,11 @@
mMaxAmplitudes = maxAmplitudes;
}
+ /** Set the duration of vendor effects in fake vibrator hardware. */
+ public void setVendorEffectDuration(long durationMs) {
+ mVendorEffectDuration = durationMs;
+ }
+
/**
* Return the amplitudes set by this controller, including zeroes for each time the vibrator was
* turned off.
@@ -366,6 +395,29 @@
}
return result;
}
+
+ /** Return list of {@link VibrationEffect.VendorEffect} played by this controller, in order. */
+ public List<VibrationEffect.VendorEffect> getVendorEffects(long vibrationId) {
+ if (mVendorEffects.containsKey(vibrationId)) {
+ return new ArrayList<>(mVendorEffects.get(vibrationId));
+ } else {
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * Returns a list of all vibrations' effect segments, for external-use where vibration IDs
+ * aren't exposed.
+ */
+ public List<VibrationEffect.VendorEffect> getAllVendorEffects() {
+ // Returns segments in order of vibrationId, which increases over time. TreeMap gives order.
+ ArrayList<VibrationEffect.VendorEffect> result = new ArrayList<>();
+ for (List<VibrationEffect.VendorEffect> subList : mVendorEffects.values()) {
+ result.addAll(subList);
+ }
+ return result;
+ }
+
/** Return list of states set for external control to the fake vibrator hardware. */
public List<Boolean> getExternalControlStates() {
return mExternalControlStates;