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;