Merge "Introduce vibrator service effect pipeline support" into main
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java
index 77d6cb7..f1d3957 100644
--- a/core/java/android/os/CombinedVibration.java
+++ b/core/java/android/os/CombinedVibration.java
@@ -17,6 +17,7 @@
 package android.os;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.os.vibrator.Flags;
 import android.util.SparseArray;
@@ -28,6 +29,7 @@
 import java.util.Locale;
 import java.util.Objects;
 import java.util.StringJoiner;
+import java.util.function.Function;
 
 /**
  * A CombinedVibration describes a combination of haptic effects to be performed by one or more
@@ -114,6 +116,17 @@
     public abstract long getDuration();
 
     /**
+     * Gets the estimated duration of the combined vibration in milliseconds.
+     *
+     * <p>For effects with hardware-dependent constants (e.g. primitive compositions), this returns
+     * the estimated duration based on the {@link VibratorInfo}. For all other effects this will
+     * return the same as {@link #getDuration()}.
+     *
+     * @hide
+     */
+    public abstract long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos);
+
+    /**
      * Returns true if this effect could represent a touch haptic feedback.
      *
      * <p>It is strongly recommended that an instance of {@link VibrationAttributes} is specified
@@ -383,6 +396,23 @@
 
         /** @hide */
         @Override
+        public long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos) {
+            if (vibratorInfos == null) {
+                return getDuration();
+            }
+            long maxDuration = 0;
+            for (int i = 0; i < vibratorInfos.size(); i++) {
+                long duration = mEffect.getDuration(vibratorInfos.valueAt(i));
+                if ((duration == Long.MAX_VALUE) || (duration < 0)) {
+                    return duration;
+                }
+                maxDuration = Math.max(maxDuration, duration);
+            }
+            return maxDuration;
+        }
+
+        /** @hide */
+        @Override
         public boolean isHapticFeedbackCandidate() {
             return mEffect.isHapticFeedbackCandidate();
         }
@@ -531,10 +561,27 @@
 
         @Override
         public long getDuration() {
+            return getDuration(idx -> mEffects.valueAt(idx).getDuration());
+        }
+
+        /** @hide */
+        @Override
+        public long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos) {
+            if (vibratorInfos == null) {
+                return getDuration();
+            }
+            return getDuration(idx -> {
+                VibrationEffect effect = mEffects.valueAt(idx);
+                VibratorInfo info = vibratorInfos.get(mEffects.keyAt(idx));
+                return effect.getDuration(info);
+            });
+        }
+
+        private long getDuration(Function<Integer, Long> durationFn) {
             long maxDuration = Long.MIN_VALUE;
             boolean hasUnknownStep = false;
             for (int i = 0; i < mEffects.size(); i++) {
-                long duration = mEffects.valueAt(i).getDuration();
+                long duration = durationFn.apply(i);
                 if (duration == Long.MAX_VALUE) {
                     // If any duration is repeating, this combination duration is also repeating.
                     return duration;
@@ -750,12 +797,21 @@
 
         @Override
         public long getDuration() {
+            return getDuration(CombinedVibration::getDuration);
+        }
+
+        /** @hide */
+        @Override
+        public long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos) {
+            return getDuration(effect -> effect.getDuration(vibratorInfos));
+        }
+
+        private long getDuration(Function<CombinedVibration, Long> durationFn) {
             boolean hasUnknownStep = false;
             long durations = 0;
             final int effectCount = mEffects.size();
             for (int i = 0; i < effectCount; i++) {
-                CombinedVibration effect = mEffects.get(i);
-                long duration = effect.getDuration();
+                long duration = durationFn.apply(mEffects.get(i));
                 if (duration == Long.MAX_VALUE) {
                     // If any duration is repeating, this combination duration is also repeating.
                     return duration;
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index ffc58c5..61dd11f 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -55,6 +55,7 @@
 import java.util.Objects;
 import java.util.StringJoiner;
 import java.util.function.BiFunction;
+import java.util.function.Function;
 
 /**
  * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}.
@@ -565,6 +566,19 @@
     public abstract long getDuration();
 
     /**
+     * Gets the estimated duration of the segment for given vibrator, in milliseconds.
+     *
+     * <p>For effects with hardware-dependent constants (e.g. primitive compositions), this returns
+     * the estimated duration based on the given {@link VibratorInfo}. For all other effects this
+     * will return the same as {@link #getDuration()}.
+     *
+     * @hide
+     */
+    public long getDuration(@Nullable VibratorInfo vibratorInfo) {
+        return getDuration();
+    }
+
+    /**
      * Checks if a vibrator with a given {@link VibratorInfo} can play this effect as intended.
      *
      * <p>See {@link VibratorInfo#areVibrationFeaturesSupported(VibrationEffect)} for more
@@ -904,13 +918,23 @@
 
         @Override
         public long getDuration() {
+            return getDuration(VibrationEffectSegment::getDuration);
+        }
+
+        /** @hide */
+        @Override
+        public long getDuration(@Nullable VibratorInfo vibratorInfo) {
+            return getDuration(segment -> segment.getDuration(vibratorInfo));
+        }
+
+        private long getDuration(Function<VibrationEffectSegment, Long> durationFn) {
             if (mRepeatIndex >= 0) {
                 return Long.MAX_VALUE;
             }
             int segmentCount = mSegments.size();
             long totalDuration = 0;
             for (int i = 0; i < segmentCount; i++) {
-                long segmentDuration = mSegments.get(i).getDuration();
+                long segmentDuration = durationFn.apply(mSegments.get(i));
                 if (segmentDuration < 0) {
                     return segmentDuration;
                 }
diff --git a/core/java/android/os/vibrator/PrebakedSegment.java b/core/java/android/os/vibrator/PrebakedSegment.java
index 39f8412..b17e82a 100644
--- a/core/java/android/os/vibrator/PrebakedSegment.java
+++ b/core/java/android/os/vibrator/PrebakedSegment.java
@@ -16,6 +16,17 @@
 
 package android.os.vibrator;
 
+import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
+import static android.os.VibrationEffect.Composition.PRIMITIVE_THUD;
+import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
+import static android.os.VibrationEffect.EFFECT_CLICK;
+import static android.os.VibrationEffect.EFFECT_DOUBLE_CLICK;
+import static android.os.VibrationEffect.EFFECT_HEAVY_CLICK;
+import static android.os.VibrationEffect.EFFECT_POP;
+import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK;
+import static android.os.VibrationEffect.EFFECT_THUD;
+import static android.os.VibrationEffect.EFFECT_TICK;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
@@ -78,6 +89,32 @@
 
     /** @hide */
     @Override
+    public long getDuration(@Nullable VibratorInfo vibratorInfo) {
+        if (vibratorInfo == null) {
+            return getDuration();
+        }
+        return switch (mEffectId) {
+            case EFFECT_TICK,
+                 EFFECT_CLICK,
+                 EFFECT_HEAVY_CLICK -> estimateFromPrimitiveDuration(vibratorInfo, PRIMITIVE_CLICK);
+            case EFFECT_TEXTURE_TICK -> estimateFromPrimitiveDuration(vibratorInfo, PRIMITIVE_TICK);
+            case EFFECT_THUD -> estimateFromPrimitiveDuration(vibratorInfo, PRIMITIVE_THUD);
+            case EFFECT_DOUBLE_CLICK -> {
+                long clickDuration = vibratorInfo.getPrimitiveDuration(PRIMITIVE_CLICK);
+                yield clickDuration > 0 ? 2 * clickDuration : getDuration();
+            }
+            default -> getDuration();
+        };
+    }
+
+    private long estimateFromPrimitiveDuration(VibratorInfo vibratorInfo, int primitiveId) {
+        int duration = vibratorInfo.getPrimitiveDuration(primitiveId);
+        // Unsupported primitives should be ignored here.
+        return duration > 0 ? duration : getDuration();
+    }
+
+    /** @hide */
+    @Override
     public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
         if (vibratorInfo.isEffectSupported(mEffectId) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES) {
             return true;
@@ -89,34 +126,30 @@
         }
         // The vibrator does not have hardware support for the effect, but the effect has fallback
         // support. Check if a fallback will be available for the effect ID.
-        switch (mEffectId) {
-            case VibrationEffect.EFFECT_CLICK:
-            case VibrationEffect.EFFECT_DOUBLE_CLICK:
-            case VibrationEffect.EFFECT_HEAVY_CLICK:
-            case VibrationEffect.EFFECT_TICK:
-                // Any of these effects are always supported via some form of fallback.
-                return true;
-            default:
-                return false;
-        }
+        return switch (mEffectId) {
+            // Any of these effects are always supported via some form of fallback.
+            case EFFECT_CLICK,
+                 EFFECT_DOUBLE_CLICK,
+                 EFFECT_HEAVY_CLICK,
+                 EFFECT_TICK -> true;
+            default -> false;
+        };
     }
 
     /** @hide */
     @Override
     public boolean isHapticFeedbackCandidate() {
-        switch (mEffectId) {
-            case VibrationEffect.EFFECT_CLICK:
-            case VibrationEffect.EFFECT_DOUBLE_CLICK:
-            case VibrationEffect.EFFECT_HEAVY_CLICK:
-            case VibrationEffect.EFFECT_POP:
-            case VibrationEffect.EFFECT_TEXTURE_TICK:
-            case VibrationEffect.EFFECT_THUD:
-            case VibrationEffect.EFFECT_TICK:
-                return true;
-            default:
-                // VibrationEffect.RINGTONES are not segments that could represent a haptic feedback
-                return false;
-        }
+        return switch (mEffectId) {
+            case EFFECT_CLICK,
+                 EFFECT_DOUBLE_CLICK,
+                 EFFECT_HEAVY_CLICK,
+                 EFFECT_POP,
+                 EFFECT_TEXTURE_TICK,
+                 EFFECT_THUD,
+                 EFFECT_TICK -> true;
+            // VibrationEffect.RINGTONES are not segments that could represent a haptic feedback
+            default -> false;
+        };
     }
 
     /** @hide */
@@ -153,27 +186,25 @@
     }
 
     private static boolean isValidEffectStrength(int strength) {
-        switch (strength) {
-            case VibrationEffect.EFFECT_STRENGTH_LIGHT:
-            case VibrationEffect.EFFECT_STRENGTH_MEDIUM:
-            case VibrationEffect.EFFECT_STRENGTH_STRONG:
-                return true;
-            default:
-                return false;
-        }
+        return switch (strength) {
+            case VibrationEffect.EFFECT_STRENGTH_LIGHT,
+                 VibrationEffect.EFFECT_STRENGTH_MEDIUM,
+                 VibrationEffect.EFFECT_STRENGTH_STRONG -> true;
+            default -> false;
+        };
     }
 
     /** @hide */
     @Override
     public void validate() {
         switch (mEffectId) {
-            case VibrationEffect.EFFECT_CLICK:
-            case VibrationEffect.EFFECT_DOUBLE_CLICK:
-            case VibrationEffect.EFFECT_HEAVY_CLICK:
-            case VibrationEffect.EFFECT_POP:
-            case VibrationEffect.EFFECT_TEXTURE_TICK:
-            case VibrationEffect.EFFECT_THUD:
-            case VibrationEffect.EFFECT_TICK:
+            case EFFECT_CLICK:
+            case EFFECT_DOUBLE_CLICK:
+            case EFFECT_HEAVY_CLICK:
+            case EFFECT_POP:
+            case EFFECT_TEXTURE_TICK:
+            case EFFECT_THUD:
+            case EFFECT_TICK:
                 break;
             default:
                 int[] ringtones = VibrationEffect.RINGTONES;
diff --git a/core/java/android/os/vibrator/PrimitiveSegment.java b/core/java/android/os/vibrator/PrimitiveSegment.java
index 3c84bcd..91653ed 100644
--- a/core/java/android/os/vibrator/PrimitiveSegment.java
+++ b/core/java/android/os/vibrator/PrimitiveSegment.java
@@ -77,6 +77,16 @@
 
     /** @hide */
     @Override
+    public long getDuration(@Nullable VibratorInfo vibratorInfo) {
+        if (vibratorInfo == null) {
+            return getDuration();
+        }
+        int duration = vibratorInfo.getPrimitiveDuration(mPrimitiveId);
+        return duration > 0 ? duration + mDelay : getDuration();
+    }
+
+    /** @hide */
+    @Override
     public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
         return vibratorInfo.isPrimitiveSupported(mPrimitiveId);
     }
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index e6e5a27..88be96a 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -86,6 +86,7 @@
     private final int mDefaultKeyboardVibrationIntensity;
 
     private final boolean mKeyboardVibrationSettingsSupported;
+    private final int mVibrationPipelineMaxDurationMs;
 
     /** @hide */
     public VibrationConfig(@Nullable Resources resources) {
@@ -106,6 +107,8 @@
                 com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger);
         mKeyboardVibrationSettingsSupported = loadBoolean(resources,
                 com.android.internal.R.bool.config_keyboardVibrationSettingsSupported);
+        mVibrationPipelineMaxDurationMs = loadInteger(resources,
+                com.android.internal.R.integer.config_vibrationPipelineMaxDuration, 0);
 
         mDefaultAlarmVibrationIntensity = loadDefaultIntensity(resources,
                 com.android.internal.R.integer.config_defaultAlarmVibrationIntensity);
@@ -221,6 +224,23 @@
     }
 
     /**
+     * The max duration, in milliseconds, allowed for pipelining vibration requests.
+     *
+     * <p>If the ongoing vibration duration is shorter than this threshold then it should be allowed
+     * to finish before the next vibration can start. If the ongoing vibration is longer than this
+     * then it should be cancelled when it's superseded for the new one.
+     *
+     * @return the max duration allowed for vibration effect to finish before the next request, or
+     * zero to disable effect pipelining.
+     */
+    public int getVibrationPipelineMaxDurationMs() {
+        if (mVibrationPipelineMaxDurationMs < 0) {
+            return 0;
+        }
+        return mVibrationPipelineMaxDurationMs;
+    }
+
+    /**
      * Whether or not vibrations are ignored if the device is on a wireless charger.
      *
      * <p>This may be the case if vibration during wireless charging causes unwanted results, like
diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java
index e1fb4e3..dadc849 100644
--- a/core/java/android/os/vibrator/VibrationEffectSegment.java
+++ b/core/java/android/os/vibrator/VibrationEffectSegment.java
@@ -17,6 +17,7 @@
 package android.os.vibrator;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -58,10 +59,23 @@
      */
     public abstract long getDuration();
 
-   /**
-     * Checks if a given {@link Vibrator} can play this segment as intended. See
-     * {@link Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more information about
-     * what counts as supported by a vibrator, and what counts as not.
+    /**
+     * Gets the estimated duration of the segment for given vibrator, in milliseconds.
+     *
+     * <p>For segments with hardware-dependent constants (e.g. primitives), this returns the
+     * estimated duration based on the given {@link VibratorInfo}. For all other effects this will
+     * return the same as {@link #getDuration()}.
+     *
+     * @hide
+     */
+    public long getDuration(@Nullable VibratorInfo vibratorInfo) {
+        return getDuration();
+    }
+
+    /**
+     * Checks if a given {@link android.os.Vibrator} can play this segment as intended. See
+     * {@link android.os.Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more
+     * information about what counts as supported by a vibrator, and what counts as not.
      *
      * @hide
      */
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index e5d891a..7ceb948 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -135,3 +135,13 @@
         purpose: PURPOSE_FEATURE
     }
 }
+
+flag {
+    namespace: "haptics"
+    name: "vibration_pipeline_enabled"
+    description: "Enables functionality to pipeline vibration effects to avoid cancelling short vibrations"
+    bug: "344494220"
+    metadata {
+        purpose: PURPOSE_FEATURE
+    }
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 169cf59..91b4820 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -287,6 +287,11 @@
          vibration params. -->
     <integer name="config_requestVibrationParamsTimeout">50</integer>
 
+    <!-- The max duration (in milliseconds) that the vibrator service will allow effects to be
+         pipelined (i.e. service will wait for ongoing vibration to finish instead of cancelling it
+         to start the new one). Value should be positive. Zero will disable effect pipelining. -->
+    <integer name="config_vibrationPipelineMaxDuration">25</integer>
+
     <!-- Array containing the usages that should request vibration params before they are played.
          These usages don't have strong latency requirements, e.g. ringtone and notification, and
          can be slightly delayed. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ab1b491..0348b46 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2141,6 +2141,7 @@
   <java-symbol type="integer" name="config_vibrationWaveformRampStepDuration" />
   <java-symbol type="bool" name="config_ignoreVibrationsOnWirelessCharger" />
   <java-symbol type="integer" name="config_vibrationWaveformRampDownDuration" />
+  <java-symbol type="integer" name="config_vibrationPipelineMaxDuration" />
   <java-symbol type="integer" name="config_radioScanningTimeout" />
   <java-symbol type="integer" name="config_requestVibrationParamsTimeout" />
   <java-symbol type="array" name="config_requestVibrationParamsForUsages" />
diff --git a/core/tests/vibrator/src/android/os/CombinedVibrationTest.java b/core/tests/vibrator/src/android/os/CombinedVibrationTest.java
index 244fcff..37ddfd2 100644
--- a/core/tests/vibrator/src/android/os/CombinedVibrationTest.java
+++ b/core/tests/vibrator/src/android/os/CombinedVibrationTest.java
@@ -22,6 +22,9 @@
 
 import static org.testng.Assert.assertThrows;
 
+import android.hardware.vibrator.IVibrator;
+import android.util.SparseArray;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -134,6 +137,54 @@
     }
 
     @Test
+    public void testDurationMono_withVibratorSupportingPrimitives() {
+        SparseArray<VibratorInfo> infos = new SparseArray<>(2);
+        infos.put(1, new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5)
+                .build());
+        infos.put(2, new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1)
+                .build());
+
+        // Use max duration from all vibrators.
+        assertEquals(10, CombinedVibration.createParallel(
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK)).getDuration(infos));
+        assertEquals(111, CombinedVibration.createParallel(
+                        VibrationEffect.startComposition()
+                                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                                .compose())
+                .getDuration(infos));
+    }
+
+    @Test
+    public void testDurationMono_withVibratorNotSupportingPrimitives() {
+        SparseArray<VibratorInfo> infos = new SparseArray<>(2);
+        infos.put(1, new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build());
+        infos.put(2, new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1)
+                .build());
+
+        // Use max duration from all vibrators.
+        assertEquals(-1, CombinedVibration.createParallel(
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK)).getDuration(infos));
+        assertEquals(-1, CombinedVibration.createParallel(
+                        VibrationEffect.startComposition()
+                                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                                .compose())
+                .getDuration(infos));
+    }
+
+    @Test
     public void testDurationStereo() {
         assertEquals(6, CombinedVibration.startParallel()
                 .addVibrator(1, VibrationEffect.createOneShot(1, 1))
@@ -156,6 +207,75 @@
     }
 
     @Test
+    public void testDurationStereo_withVibratorSupportingPrimitives() {
+        SparseArray<VibratorInfo> infos = new SparseArray<>(2);
+        infos.put(1, new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5)
+                .build());
+        infos.put(2, new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1)
+                .build());
+
+        // Use specific vibrator durations, then max effect duration
+        assertEquals(111, CombinedVibration.startParallel()
+                .addVibrator(1, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .addVibrator(2, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .combine()
+                .getDuration(infos));
+        assertEquals(110, CombinedVibration.startParallel()
+                .addVibrator(1, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .combine()
+                .getDuration(infos));
+    }
+
+    @Test
+    public void testDurationStereo_withVibratorNotSupportingPrimitives() {
+        SparseArray<VibratorInfo> infos = new SparseArray<>(2);
+        infos.put(1, new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build());
+        infos.put(2, new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1)
+                .build());
+
+        // One vibrator does not support primitives
+        assertEquals(-1, CombinedVibration.startParallel()
+                .addVibrator(1, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .addVibrator(2, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .combine()
+                .getDuration(infos));
+        // Invalid vibrator ID
+        assertEquals(-1, CombinedVibration.startParallel()
+                .addVibrator(3, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .combine()
+                .getDuration(infos));
+    }
+
+    @Test
     public void testDurationSequential() {
         assertEquals(26, CombinedVibration.startSequential()
                 .addNext(1, VibrationEffect.createOneShot(10, 10), 10)
@@ -178,6 +298,59 @@
     }
 
     @Test
+    public void testDurationSequential_withVibratorSupportingPrimitives() {
+        SparseArray<VibratorInfo> infos = new SparseArray<>(2);
+        infos.put(1, new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5)
+                .build());
+        infos.put(2, new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1)
+                .build());
+
+        // Add each duration and delay
+        assertEquals(321, CombinedVibration.startSequential()
+                .addNext(1, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose(), 100)
+                .addNext(2, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .combine()
+                .getDuration(infos));
+    }
+
+    @Test
+    public void testDurationSequential_withVibratorNotSupportingPrimitives() {
+        SparseArray<VibratorInfo> infos = new SparseArray<>(2);
+        infos.put(1, new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build());
+        infos.put(2, new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1)
+                .build());
+
+        assertEquals(-1, CombinedVibration.startSequential()
+                .addNext(1, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose(), 100)
+                .addNext(2, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                        .compose())
+                .combine()
+                .getDuration(infos));
+    }
+
+    @Test
     public void testIsHapticFeedbackCandidateMono() {
         assertTrue(CombinedVibration.createParallel(
                 VibrationEffect.createOneShot(1, 1)).isHapticFeedbackCandidate());
diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
index f5b04ee..8acf2ed 100644
--- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java
+++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
@@ -1078,6 +1078,52 @@
     }
 
     @Test
+    public void testDuration_withVibratorSupportingPrimitives() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5)
+                .build();
+
+        VibrationEffect composition = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100)
+                .compose();
+
+        assertEquals(1, VibrationEffect.createOneShot(1, 1).getDuration());
+        assertEquals(10, VibrationEffect.get(VibrationEffect.EFFECT_CLICK).getDuration(info));
+        assertEquals(115, composition.getDuration(info));
+        assertEquals(Long.MAX_VALUE,
+                VibrationEffect.startComposition()
+                        .repeatEffectIndefinitely(composition)
+                        .compose()
+                        .getDuration(info));
+        if (Flags.vendorVibrationEffects()) {
+            assertEquals(-1,
+                    VibrationEffect.createVendorEffect(createNonEmptyBundle()).getDuration(info));
+        }
+    }
+
+    @Test
+    public void testDuration_withVibratorNotSupportingPrimitives() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build();
+
+        VibrationEffect composition = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                .compose();
+
+        assertEquals(-1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK).getDuration(info));
+        assertEquals(-1, composition.getDuration(info));
+        assertEquals(Long.MAX_VALUE,
+                VibrationEffect.startComposition()
+                        .repeatEffectIndefinitely(composition)
+                        .compose()
+                        .getDuration(info));
+    }
+
+    @Test
     public void testAreVibrationFeaturesSupported_allSegmentsSupported() {
         VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
diff --git a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java
index 7dd9e55..f9ec5f0 100644
--- a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java
@@ -24,6 +24,7 @@
 import static org.testng.Assert.assertNotEquals;
 import static org.testng.Assert.assertThrows;
 
+import android.hardware.vibrator.IVibrator;
 import android.os.Parcel;
 import android.os.VibrationEffect;
 import android.os.VibratorInfo;
@@ -114,39 +115,82 @@
 
     @Test
     public void testDuration() {
-        assertEquals(-1, new PrebakedSegment(
-                VibrationEffect.EFFECT_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
-                .getDuration());
-        assertEquals(-1, new PrebakedSegment(
-                VibrationEffect.EFFECT_TICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
-                .getDuration());
-        assertEquals(-1, new PrebakedSegment(
-                VibrationEffect.EFFECT_DOUBLE_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
-                .getDuration());
-        assertEquals(-1, new PrebakedSegment(
-                VibrationEffect.EFFECT_THUD, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_CLICK).getDuration());
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_TICK).getDuration());
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_THUD).getDuration());
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
                 .getDuration());
     }
 
     @Test
+    public void testDuration_withVibratorSupportingPrimitives_returnsPrimitiveDuration() {
+        int tickDuration = 5;
+        int clickDuration = 10;
+        int thudDuration = 15;
+
+        VibratorInfo vibratorInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, tickDuration)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, clickDuration)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, thudDuration)
+                .build();
+
+        assertEquals(5, createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK)
+                .getDuration(vibratorInfo));
+        assertEquals(10, createSegmentWithFallback(VibrationEffect.EFFECT_TICK)
+                .getDuration(vibratorInfo));
+        assertEquals(10, createSegmentWithFallback(VibrationEffect.EFFECT_CLICK)
+                .getDuration(vibratorInfo));
+        assertEquals(10, createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK)
+                .getDuration(vibratorInfo));
+        assertEquals(20, createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
+                .getDuration(vibratorInfo));
+        assertEquals(15, createSegmentWithFallback(VibrationEffect.EFFECT_THUD)
+                .getDuration(vibratorInfo));
+
+        // Unknown effects
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_POP)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.RINGTONES[0])
+                .getDuration(vibratorInfo));
+    }
+
+    @Test
+    public void testDuration_withVibratorNotSupportingPrimitives_returnsUnknown() {
+        VibratorInfo vibratorInfo = createVibratorInfoWithSupportedEffects(
+                VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_POP);
+
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_TICK)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_CLICK)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_THUD)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_POP)
+                .getDuration(vibratorInfo));
+        assertEquals(-1, createSegmentWithFallback(VibrationEffect.RINGTONES[0])
+                .getDuration(vibratorInfo));
+    }
+
+    @Test
     public void testIsHapticFeedbackCandidate_prebakedConstants_areCandidates() {
-        assertTrue(new PrebakedSegment(
-                VibrationEffect.EFFECT_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_CLICK)
                 .isHapticFeedbackCandidate());
-        assertTrue(new PrebakedSegment(
-                VibrationEffect.EFFECT_TICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TICK)
                 .isHapticFeedbackCandidate());
-        assertTrue(new PrebakedSegment(
-                VibrationEffect.EFFECT_DOUBLE_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
                 .isHapticFeedbackCandidate());
-        assertTrue(new PrebakedSegment(
-                VibrationEffect.EFFECT_HEAVY_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK)
                 .isHapticFeedbackCandidate());
-        assertTrue(new PrebakedSegment(
-                VibrationEffect.EFFECT_THUD, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_THUD)
                 .isHapticFeedbackCandidate());
-        assertTrue(new PrebakedSegment(
-                VibrationEffect.EFFECT_TEXTURE_TICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK)
                 .isHapticFeedbackCandidate());
     }
 
@@ -271,8 +315,7 @@
 
     @Test
     public void testIsHapticFeedbackCandidate_prebakedRingtones_notCandidates() {
-        assertFalse(new PrebakedSegment(
-                VibrationEffect.RINGTONES[1], true, VibrationEffect.EFFECT_STRENGTH_MEDIUM)
+        assertFalse(createSegmentWithFallback(VibrationEffect.RINGTONES[1])
                 .isHapticFeedbackCandidate());
     }
 
diff --git a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
index 97f1d5e..a6d9dc5 100644
--- a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
@@ -201,6 +201,22 @@
     }
 
     @Test
+    public void testDuration_withVibratorSupportingPrimitives_returnsVibratorDurationWithDelay() {
+        VibratorInfo vibratorInfo = createVibratorInfoWithSupportedPrimitive(
+                VibrationEffect.Composition.PRIMITIVE_CLICK, /* durationMs= */ 10);
+        assertEquals(15, new PrimitiveSegment(
+                VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 5).getDuration(vibratorInfo));
+    }
+
+    @Test
+    public void testDuration_withVibratorNotSupportingPrimitive_returnsUnknown() {
+        VibratorInfo vibratorInfo = createVibratorInfoWithSupportedPrimitive(
+                VibrationEffect.Composition.PRIMITIVE_CLICK);
+        assertEquals(-1, new PrimitiveSegment(
+                VibrationEffect.Composition.PRIMITIVE_NOOP, 1, 5).getDuration(vibratorInfo));
+    }
+
+    @Test
     public void testVibrationFeaturesSupport_primitiveSupportedByVibrator() {
         assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_CLICK)
                 .areVibrationFeaturesSupported(
@@ -252,9 +268,14 @@
     }
 
     private static VibratorInfo createVibratorInfoWithSupportedPrimitive(int primitiveId) {
+        return createVibratorInfoWithSupportedPrimitive(primitiveId, /* durationMs= */ 10);
+    }
+
+    private static VibratorInfo createVibratorInfoWithSupportedPrimitive(int primitiveId,
+            int durationMs) {
         return new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(primitiveId, 10)
+                .setSupportedPrimitive(primitiveId, durationMs)
                 .build();
     }
 }
diff --git a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
index bea8293..df874bcb 100644
--- a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
@@ -195,7 +195,14 @@
 
     @Test
     public void testDuration() {
+        VibratorInfo infoWithSupport =
+                createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true);
+        VibratorInfo infoWithoutSupport =
+                createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ false);
+
         assertEquals(10, new RampSegment(0.5f, 1, 0, 0, 10).getDuration());
+        assertEquals(10, new RampSegment(0.5f, 1, 0, 0, 10).getDuration(infoWithSupport));
+        assertEquals(10, new RampSegment(0.5f, 1, 0, 0, 10).getDuration(infoWithoutSupport));
     }
 
     @Test
diff --git a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
index 411074a..914117c 100644
--- a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
@@ -213,7 +213,13 @@
 
     @Test
     public void testDuration() {
+        VibratorInfo infoWithSupport = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true);
+        VibratorInfo infoWithoutSupport =
+                createVibInfoForAmplitude(/* hasAmplitudeControl= */ false);
+
         assertEquals(5, new StepSegment(0, 0, 5).getDuration());
+        assertEquals(5, new StepSegment(0, 0, 5).getDuration(infoWithSupport));
+        assertEquals(5, new StepSegment(0, 0, 5).getDuration(infoWithoutSupport));
     }
 
     @Test
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index fbcc856..d192e64 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -21,6 +21,8 @@
 import android.os.CombinedVibration;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
+import android.os.VibratorInfo;
+import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.VibrationEffectSegment;
 import android.util.SparseArray;
@@ -145,19 +147,30 @@
                 originalEffect, mScaleLevel, mAdaptiveScale);
     }
 
-    /**
-     * Returns true if this vibration can pipeline with the specified one.
-     *
-     * <p>Note that currently, repeating vibrations can't pipeline with following vibrations,
-     * because the cancel() call to stop the repetition will cancel a pending vibration too. This
-     * can be changed if we have a use-case to reason around behavior for. It may also be nice to
-     * pipeline very short vibrations together, regardless of the flag.
-     */
-    public boolean canPipelineWith(HalVibration vib) {
-        return callerInfo.uid == vib.callerInfo.uid && callerInfo.attrs.isFlagSet(
-                VibrationAttributes.FLAG_PIPELINED_EFFECT)
-                && vib.callerInfo.attrs.isFlagSet(VibrationAttributes.FLAG_PIPELINED_EFFECT)
-                && (mOriginalEffect.getDuration() != Long.MAX_VALUE);
+    /** Returns true if this vibration can pipeline with the specified one. */
+    public boolean canPipelineWith(HalVibration vib,
+            @Nullable SparseArray<VibratorInfo> vibratorInfos, int durationThresholdMs) {
+        long effectDuration = Flags.vibrationPipelineEnabled() && (vibratorInfos != null)
+                ? mEffectToPlay.getDuration(vibratorInfos)
+                : mEffectToPlay.getDuration();
+        if (effectDuration == Long.MAX_VALUE) {
+            // Repeating vibrations can't pipeline with following vibrations, because the cancel()
+            // call to stop the repetition will cancel a pending vibration too. This can be changed
+            // if we have a use-case, requiring changes to how pipelined vibrations are cancelled.
+            return false;
+        }
+        if (Flags.vibrationPipelineEnabled()
+                && (effectDuration > 0) && (effectDuration < durationThresholdMs)) {
+            // Duration is known and it's less than the pipeline threshold, so allow it.
+            // No need to check UID, as we want to avoid cancelling any short effect and let the
+            // vibrator hardware gracefully finish the vibration.
+            return true;
+        }
+        // Check the same app is requesting multiple vibrations with the pipeline flag,
+        // independently of the effect durations.
+        return callerInfo.uid == vib.callerInfo.uid
+                && callerInfo.attrs.isFlagSet(VibrationAttributes.FLAG_PIPELINED_EFFECT)
+                && vib.callerInfo.attrs.isFlagSet(VibrationAttributes.FLAG_PIPELINED_EFFECT);
     }
 
     private void fillFallbacksForEffect(CombinedVibration effect,
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 9b7bdec..7d5d34d 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -168,12 +168,15 @@
 
     @VisibleForTesting
     final VibrationSettings mVibrationSettings;
+    private final VibrationConfig mVibrationConfig;
     private final VibrationScaler mVibrationScaler;
     private final VibratorControlService mVibratorControlService;
     private final InputDeviceDelegate mInputDeviceDelegate;
     private final DeviceAdapter mDeviceAdapter;
 
     @GuardedBy("mLock")
+    @Nullable private SparseArray<VibratorInfo> mVibratorInfos;
+    @GuardedBy("mLock")
     @Nullable private VibratorInfo mCombinedVibratorInfo;
     @GuardedBy("mLock")
     @Nullable private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider;
@@ -247,9 +250,9 @@
         mHandler = injector.createHandler(Looper.myLooper());
         mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler);
 
-        VibrationConfig vibrationConfig = new VibrationConfig(context.getResources());
-        mVibrationSettings = new VibrationSettings(mContext, mHandler, vibrationConfig);
-        mVibrationScaler = new VibrationScaler(vibrationConfig, mVibrationSettings);
+        mVibrationConfig = new VibrationConfig(context.getResources());
+        mVibrationSettings = new VibrationSettings(mContext, mHandler, mVibrationConfig);
+        mVibrationScaler = new VibrationScaler(mVibrationConfig, mVibrationSettings);
         mVibratorControlService = new VibratorControlService(mContext,
                 injector.createVibratorControllerHolder(), mVibrationScaler, mVibrationSettings,
                 mFrameworkStatsLogger, mLock);
@@ -295,7 +298,9 @@
             mVibratorIds = vibratorIds;
             mVibrators = new SparseArray<>(mVibratorIds.length);
             for (int vibratorId : vibratorIds) {
-                mVibrators.put(vibratorId, injector.createVibratorController(vibratorId, listener));
+                VibratorController vibratorController =
+                        injector.createVibratorController(vibratorId, listener);
+                mVibrators.put(vibratorId, vibratorController);
             }
         }
 
@@ -334,6 +339,15 @@
                 mVibrators.valueAt(i).reloadVibratorInfoIfNeeded();
             }
 
+            synchronized (mLock) {
+                mVibratorInfos = transformAllVibratorsLocked(VibratorController::getVibratorInfo);
+                VibratorInfo[] infos = new VibratorInfo[mVibratorInfos.size()];
+                for (int i = 0; i < mVibratorInfos.size(); i++) {
+                    infos[i] = mVibratorInfos.valueAt(i);
+                }
+                mCombinedVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, infos);
+            }
+
             mVibrationSettings.onSystemReady();
             mInputDeviceDelegate.onSystemReady();
 
@@ -633,7 +647,8 @@
                         endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, callerInfo,
                                 /* continueExternalControl= */ false);
                     } else if (mCurrentVibration != null) {
-                        if (mCurrentVibration.getVibration().canPipelineWith(vib)) {
+                        if (mCurrentVibration.getVibration().canPipelineWith(vib, mVibratorInfos,
+                                mVibrationConfig.getVibrationPipelineMaxDurationMs())) {
                             // Don't cancel the current vibration if it's pipeline-able.
                             // Note that if there is a pending next vibration that can't be
                             // pipelined, it will have already cancelled the current one, so we
@@ -1871,33 +1886,11 @@
         }
     }
 
+    @Nullable
     private VibratorInfo getCombinedVibratorInfo() {
         synchronized (mLock) {
-            // Used a cached resolving vibrator if one exists.
-            if (mCombinedVibratorInfo != null) {
-                return mCombinedVibratorInfo;
-            }
-
-            // Return an empty resolving vibrator if the service has no vibrator.
-            if (mVibratorIds.length == 0) {
-                return mCombinedVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO;
-            }
-
-            // Combine the vibrator infos of all the service's vibrator to create a single resolving
-            // vibrator that is based on the combined info.
-            VibratorInfo[] infos = new VibratorInfo[mVibratorIds.length];
-            for (int i = 0; i < mVibratorIds.length; i++) {
-                VibratorInfo info = getVibratorInfo(mVibratorIds[i]);
-                // If any one of the service's vibrator does not have a valid vibrator info, stop
-                // trying to create and cache a combined resolving vibrator. Combine the infos only
-                // when infos for all vibrators are available.
-                if (info == null) {
-                    return null;
-                }
-                infos[i] = info;
-            }
-
-            return mCombinedVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, infos);
+            // This is only initialized at system ready, when all vibrator infos are fully loaded.
+            return mCombinedVibratorInfo;
         }
     }
 
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 b782162..7f5da41 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.vibrator;
 
-import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -28,6 +26,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -89,17 +88,14 @@
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.view.HapticFeedbackConstants;
 import android.view.InputDevice;
-import android.view.flags.Flags;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -168,9 +164,7 @@
     @Rule
     public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
     @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
-    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Mock
     private VibratorManagerService.NativeWrapper mNativeWrapperMock;
@@ -800,7 +794,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_CANCEL_BY_APPOPS)
+    @EnableFlags(android.os.vibrator.Flags.FLAG_CANCEL_BY_APPOPS)
     public void vibrate_thenDeniedAppOps_getsCancelled() throws Throwable {
         mockVibrators(1);
         VibratorManagerService service = createSystemReadyService();
@@ -894,7 +888,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS)
+    @EnableFlags(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS)
     public void vibrate_thenFgUserRequestsMute_getsCancelled() throws Throwable {
         mockVibrators(1);
         VibratorManagerService service = createSystemReadyService();
@@ -1331,6 +1325,37 @@
     }
 
     @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VIBRATION_PIPELINE_ENABLED)
+    public void vibrate_withPipelineFlagEnabledAndShortEffect_continuesOngoingEffect()
+            throws Exception {
+        assumeTrue(mVibrationConfig.getVibrationPipelineMaxDurationMs() > 0);
+
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        fakeVibrator.setSupportedPrimitives(
+                VibrationEffect.Composition.PRIMITIVE_CLICK,
+                VibrationEffect.Composition.PRIMITIVE_THUD);
+        fakeVibrator.setPrimitiveDuration(
+                mVibrationConfig.getVibrationPipelineMaxDurationMs() - 1);
+        VibratorManagerService service = createSystemReadyService();
+
+        HalVibration firstVibration = vibrateWithUid(service, /* uid= */ 123,
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .compose(), HAPTIC_FEEDBACK_ATTRS);
+        HalVibration secondVibration = vibrateWithUid(service, /* uid= */ 456,
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD)
+                        .compose(), HAPTIC_FEEDBACK_ATTRS);
+        secondVibration.waitForEnd();
+
+        assertThat(fakeVibrator.getAllEffectSegments()).hasSize(2);
+        assertThat(firstVibration.getStatus()).isEqualTo(Status.FINISHED);
+        assertThat(secondVibration.getStatus()).isEqualTo(Status.FINISHED);
+    }
+
+    @Test
     public void vibrate_withInputDevices_vibratesInputDevices() throws Exception {
         mockVibrators(1);
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
@@ -1512,6 +1537,7 @@
     }
 
     @Test
+    @EnableFlags(android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API)
     public void performHapticFeedback_doesNotRequireVibrateOrBypassPermissions() throws Exception {
         // Deny permissions that would have been required for regular vibrations, and check that
         // the vibration proceed as expected to verify that haptic feedback does not need these
@@ -1520,8 +1546,6 @@
         denyPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS);
         denyPermission(android.Manifest.permission.MODIFY_PHONE_STATE);
         denyPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING);
-        // Flag override to enable the scroll feedack constants to bypass interruption policies.
-        mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API);
         mHapticFeedbackVibrationMap.put(
                 HapticFeedbackConstants.SCROLL_TICK,
                 VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
@@ -1544,6 +1568,10 @@
     }
 
     @Test
+    @EnableFlags({
+            android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API,
+            android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED,
+    })
     public void performHapticFeedbackForInputDevice_doesNotRequireVibrateOrBypassPermissions()
             throws Exception {
         // Deny permissions that would have been required for regular vibrations, and check that
@@ -1553,9 +1581,6 @@
         denyPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS);
         denyPermission(android.Manifest.permission.MODIFY_PHONE_STATE);
         denyPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING);
-        // Flag override to enable the scroll feedback constants to bypass interruption policies.
-        mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API);
-        mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED);
         mHapticFeedbackVibrationMapSourceRotary.put(
                 HapticFeedbackConstants.SCROLL_TICK,
                 VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
@@ -1628,12 +1653,14 @@
     }
 
     @Test
+    @EnableFlags({
+            android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API,
+            android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED,
+    })
     public void performHapticFeedbackForInputDevice_restrictedConstantsWithoutPermission_doesNotVibrate()
             throws Exception {
         // Deny permission to vibrate with restricted constants
         denyPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS);
-        mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API);
-        mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED);
         // Public constant, no permission required
         mHapticFeedbackVibrationMapSourceRotary.put(
                 HapticFeedbackConstants.CONFIRM,
@@ -1697,9 +1724,9 @@
     }
 
     @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED)
     public void performHapticFeedbackForInputDevice_restrictedConstantsWithPermission_playsVibration()
             throws Exception {
-        mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED);
         // Grant permission to vibrate with restricted constants
         grantPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS);
         // Public constant, no permission required
@@ -1732,9 +1759,11 @@
     }
 
     @Test
+    @EnableFlags({
+            android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API,
+            android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED,
+    })
     public void performHapticFeedback_doesNotVibrateWhenVibratorInfoNotReady() throws Exception {
-        mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API);
-        mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED);
         denyPermission(android.Manifest.permission.VIBRATE);
         mHapticFeedbackVibrationMap.put(
                 HapticFeedbackConstants.KEYBOARD_TAP,
@@ -1767,9 +1796,11 @@
     }
 
     @Test
+    @EnableFlags({
+            android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API,
+            android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED,
+    })
     public void performHapticFeedback_doesNotVibrateForInvalidConstant() throws Exception {
-        mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API);
-        mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED);
         denyPermission(android.Manifest.permission.VIBRATE);
         mockVibrators(1);
         VibratorManagerService service = createSystemReadyService();
@@ -1791,7 +1822,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @EnableFlags(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);
@@ -1816,7 +1847,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
     public void vibrate_vendorEffectsWithPermission_successful() throws Exception {
         // Grant permission to vibrate with vendor effects
         grantPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS);
@@ -1904,7 +1935,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+    @EnableFlags(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
     public void vibrate_withAdaptiveHaptics_appliesCorrectAdaptiveScales() throws Exception {
         // Keep user settings the same as device default so only adaptive scale is applied.
         setUserSetting(Settings.System.ALARM_VIBRATION_INTENSITY,
@@ -1947,7 +1978,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({
+    @EnableFlags({
             android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
             android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS,
     })
@@ -2418,7 +2449,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+    @EnableFlags(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
     public void onExternalVibration_withAdaptiveHaptics_returnsCorrectAdaptiveScales() {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL,
@@ -2465,7 +2496,7 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+    @DisableFlags(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
     public void onExternalVibration_withAdaptiveHapticsFlagDisabled_alwaysReturnScaleNone() {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL,
@@ -2585,7 +2616,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS)
+    @EnableFlags(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS)
     public void onExternalVibration_thenFgUserRequestsMute_doNotCancelVibration() throws Throwable {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
@@ -3105,9 +3136,20 @@
         return vibrateWithDevice(service, Context.DEVICE_ID_DEFAULT, effect, attrs);
     }
 
+    private HalVibration vibrateWithUid(VibratorManagerService service, int uid,
+            VibrationEffect effect, VibrationAttributes attrs) {
+        return vibrateWithUidAndDevice(service, uid, Context.DEVICE_ID_DEFAULT,
+                CombinedVibration.createParallel(effect), attrs);
+    }
+
     private HalVibration vibrateWithDevice(VibratorManagerService service, int deviceId,
             CombinedVibration effect, VibrationAttributes attrs) {
-        HalVibration vib = service.vibrateWithPermissionCheck(UID, deviceId, PACKAGE_NAME, effect,
+        return vibrateWithUidAndDevice(service, UID, deviceId, effect, attrs);
+    }
+
+    private HalVibration vibrateWithUidAndDevice(VibratorManagerService service, int uid,
+            int deviceId, CombinedVibration effect, VibrationAttributes attrs) {
+        HalVibration vib = service.vibrateWithPermissionCheck(uid, deviceId, PACKAGE_NAME, effect,
                 attrs, "some reason", service);
         if (vib != null) {
             mPendingVibrations.add(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 6dc1b10..75a9cedf 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -80,6 +80,7 @@
     private float[] mFrequenciesHz;
     private float[] mOutputAccelerationsGs;
     private long mVendorEffectDuration = EFFECT_DURATION;
+    private long mPrimitiveDuration = EFFECT_DURATION;
 
     void recordEffectSegment(long vibrationId, VibrationEffectSegment segment) {
         mEffectSegments.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(segment);
@@ -171,7 +172,7 @@
             }
             long duration = 0;
             for (PrimitiveSegment primitive : primitives) {
-                duration += EFFECT_DURATION + primitive.getDelay();
+                duration += mPrimitiveDuration + primitive.getDelay();
                 recordEffectSegment(vibrationId, primitive);
             }
             applyLatency(mOnLatency);
@@ -381,6 +382,11 @@
         mVendorEffectDuration = durationMs;
     }
 
+    /** Set the duration of primitives in fake vibrator hardware. */
+    public void setPrimitiveDuration(long primitiveDuration) {
+        mPrimitiveDuration = primitiveDuration;
+    }
+
     /**
      * Set the maximum number of envelope effects control points supported in fake vibrator
      * hardware.