Merge "Create single thread to play any Vibration"
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 08b73278..e885fd3 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1366,11 +1366,10 @@
 
   public static class VibrationEffect.Prebaked extends android.os.VibrationEffect implements android.os.Parcelable {
     ctor public VibrationEffect.Prebaked(android.os.Parcel);
-    ctor public VibrationEffect.Prebaked(int, boolean);
+    ctor public VibrationEffect.Prebaked(int, boolean, int);
     method public long getDuration();
     method public int getEffectStrength();
     method public int getId();
-    method public void setEffectStrength(int);
     method public boolean shouldFallback();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect.Prebaked> CREATOR;
diff --git a/core/java/android/os/CombinedVibrationEffect.java b/core/java/android/os/CombinedVibrationEffect.java
index 7ec7fff..869a727 100644
--- a/core/java/android/os/CombinedVibrationEffect.java
+++ b/core/java/android/os/CombinedVibrationEffect.java
@@ -87,8 +87,14 @@
     }
 
     /** @hide */
+    public abstract long getDuration();
+
+    /** @hide */
     public abstract void validate();
 
+    /** @hide */
+    public abstract boolean hasVibrator(int vibratorId);
+
     /**
      * A combination of haptic effects that should be played in multiple vibrators in sync.
      *
@@ -265,6 +271,11 @@
             return mEffect;
         }
 
+        @Override
+        public long getDuration() {
+            return mEffect.getDuration();
+        }
+
         /** @hide */
         @Override
         public void validate() {
@@ -272,12 +283,17 @@
         }
 
         @Override
+        public boolean hasVibrator(int vibratorId) {
+            return true;
+        }
+
+        @Override
         public boolean equals(Object o) {
             if (!(o instanceof Mono)) {
                 return false;
             }
             Mono other = (Mono) o;
-            return other.mEffect.equals(other.mEffect);
+            return mEffect.equals(other.mEffect);
         }
 
         @Override
@@ -345,6 +361,15 @@
             return mEffects;
         }
 
+        @Override
+        public long getDuration() {
+            long maxDuration = Long.MIN_VALUE;
+            for (int i = 0; i < mEffects.size(); i++) {
+                maxDuration = Math.max(maxDuration, mEffects.valueAt(i).getDuration());
+            }
+            return maxDuration;
+        }
+
         /** @hide */
         @Override
         public void validate() {
@@ -356,6 +381,11 @@
         }
 
         @Override
+        public boolean hasVibrator(int vibratorId) {
+            return mEffects.indexOfKey(vibratorId) >= 0;
+        }
+
+        @Override
         public boolean equals(Object o) {
             if (!(o instanceof Stereo)) {
                 return false;
@@ -445,6 +475,26 @@
             return mDelays;
         }
 
+        @Override
+        public long getDuration() {
+            long durations = 0;
+            final int effectCount = mEffects.size();
+            for (int i = 0; i < effectCount; i++) {
+                CombinedVibrationEffect effect = mEffects.get(i);
+                long duration = effect.getDuration();
+                if (duration < 0) {
+                    // If any duration is unknown, this combination duration is also unknown.
+                    return duration;
+                }
+                durations += duration;
+            }
+            long delays = 0;
+            for (int i = 0; i < effectCount; i++) {
+                delays += mDelays.get(i);
+            }
+            return durations + delays;
+        }
+
         /** @hide */
         @Override
         public void validate() {
@@ -452,13 +502,15 @@
                     "There should be at least one effect set for a combined effect");
             Preconditions.checkArgument(mEffects.size() == mDelays.size(),
                     "Effect and delays should have equal length");
-            for (long delay : mDelays) {
-                if (delay < 0) {
+            final int effectCount = mEffects.size();
+            for (int i = 0; i < effectCount; i++) {
+                if (mDelays.get(i) < 0) {
                     throw new IllegalArgumentException("Delays must all be >= 0"
                             + " (delays=" + mDelays + ")");
                 }
             }
-            for (CombinedVibrationEffect effect : mEffects) {
+            for (int i = 0; i < effectCount; i++) {
+                CombinedVibrationEffect effect = mEffects.get(i);
                 if (effect instanceof Sequential) {
                     throw new IllegalArgumentException(
                             "There should be no nested sequential effects in a combined effect");
@@ -468,6 +520,17 @@
         }
 
         @Override
+        public boolean hasVibrator(int vibratorId) {
+            final int effectCount = mEffects.size();
+            for (int i = 0; i < effectCount; i++) {
+                if (mEffects.get(i).hasVibrator(vibratorId)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
         public boolean equals(Object o) {
             if (!(o instanceof Sequential)) {
                 return false;
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index c0b2ada..df3beb2 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -317,7 +317,7 @@
      */
     @TestApi
     public static VibrationEffect get(int effectId, boolean fallback) {
-        VibrationEffect effect = new Prebaked(effectId, fallback);
+        VibrationEffect effect = new Prebaked(effectId, fallback, EffectStrength.MEDIUM);
         effect.validate();
         return effect;
     }
@@ -792,22 +792,30 @@
     public static class Prebaked extends VibrationEffect implements Parcelable {
         private final int mEffectId;
         private final boolean mFallback;
-
-        private int mEffectStrength;
+        private final int mEffectStrength;
+        @Nullable
+        private final VibrationEffect mFallbackEffect;
 
         public Prebaked(Parcel in) {
-            this(in.readInt(), in.readByte() != 0, in.readInt());
+            mEffectId = in.readInt();
+            mFallback = in.readByte() != 0;
+            mEffectStrength = in.readInt();
+            mFallbackEffect = in.readParcelable(VibrationEffect.class.getClassLoader());
         }
 
-        public Prebaked(int effectId, boolean fallback) {
-            this(effectId, fallback, EffectStrength.MEDIUM);
-        }
-
-        /** @hide */
         public Prebaked(int effectId, boolean fallback, int effectStrength) {
             mEffectId = effectId;
             mFallback = fallback;
             mEffectStrength = effectStrength;
+            mFallbackEffect = null;
+        }
+
+        /** @hide */
+        public Prebaked(int effectId, int effectStrength, @NonNull VibrationEffect fallbackEffect) {
+            mEffectId = effectId;
+            mFallback = true;
+            mEffectStrength = effectStrength;
+            mFallbackEffect = fallbackEffect;
         }
 
         public int getId() {
@@ -829,26 +837,27 @@
 
         /** @hide */
         @Override
-        public VibrationEffect resolve(int defaultAmplitude) {
-            // Prebaked effects already have default amplitude set, so ignore this.
+        public Prebaked resolve(int defaultAmplitude) {
+            if (mFallbackEffect != null) {
+                VibrationEffect resolvedFallback = mFallbackEffect.resolve(defaultAmplitude);
+                if (!mFallbackEffect.equals(resolvedFallback)) {
+                    return new Prebaked(mEffectId, mEffectStrength, resolvedFallback);
+                }
+            }
             return this;
         }
 
         /** @hide */
         @Override
         public Prebaked scale(float scaleFactor) {
-            // Prebaked effects cannot be scaled, so ignore this.
-            return this;
-        }
-
-        /**
-         * Set the effect strength of the prebaked effect.
-         */
-        public void setEffectStrength(int strength) {
-            if (!isValidEffectStrength(strength)) {
-                throw new IllegalArgumentException("Invalid effect strength: " + strength);
+            if (mFallbackEffect != null) {
+                VibrationEffect scaledFallback = mFallbackEffect.scale(scaleFactor);
+                if (!mFallbackEffect.equals(scaledFallback)) {
+                    return new Prebaked(mEffectId, mEffectStrength, scaledFallback);
+                }
             }
-            mEffectStrength = strength;
+            // Prebaked effect strength cannot be scaled with this method.
+            return this;
         }
 
         /**
@@ -858,6 +867,16 @@
             return mEffectStrength;
         }
 
+        /**
+         * Return the fallback effect, if set.
+         *
+         * @hide
+         */
+        @Nullable
+        public VibrationEffect getFallbackEffect() {
+            return mFallbackEffect;
+        }
+
         private static boolean isValidEffectStrength(int strength) {
             switch (strength) {
                 case EffectStrength.LIGHT:
@@ -901,15 +920,13 @@
             VibrationEffect.Prebaked other = (VibrationEffect.Prebaked) o;
             return mEffectId == other.mEffectId
                 && mFallback == other.mFallback
-                && mEffectStrength == other.mEffectStrength;
+                && mEffectStrength == other.mEffectStrength
+                && Objects.equals(mFallbackEffect, other.mFallbackEffect);
         }
 
         @Override
         public int hashCode() {
-            int result = 17;
-            result += 37 * mEffectId;
-            result += 37 * mEffectStrength;
-            return result;
+            return Objects.hash(mEffectId, mFallback, mEffectStrength, mFallbackEffect);
         }
 
         @Override
@@ -917,6 +934,7 @@
             return "Prebaked{mEffectId=" + mEffectId
                 + ", mEffectStrength=" + mEffectStrength
                 + ", mFallback=" + mFallback
+                + ", mFallbackEffect=" + mFallbackEffect
                 + "}";
         }
 
@@ -927,6 +945,7 @@
             out.writeInt(mEffectId);
             out.writeByte((byte) (mFallback ? 1 : 0));
             out.writeInt(mEffectStrength);
+            out.writeParcelable(mFallbackEffect, flags);
         }
 
         public static final @NonNull Parcelable.Creator<Prebaked> CREATOR =
@@ -990,8 +1009,10 @@
                 // Just return this if there's no scaling to be done.
                 return this;
             }
+            final int primitiveCount = mPrimitiveEffects.size();
             List<Composition.PrimitiveEffect> scaledPrimitives = new ArrayList<>();
-            for (Composition.PrimitiveEffect primitive : mPrimitiveEffects) {
+            for (int i = 0; i < primitiveCount; i++) {
+                Composition.PrimitiveEffect primitive = mPrimitiveEffects.get(i);
                 scaledPrimitives.add(new Composition.PrimitiveEffect(
                         primitive.id, scale(primitive.scale, scaleFactor), primitive.delay));
             }
@@ -1001,11 +1022,12 @@
         /** @hide */
         @Override
         public void validate() {
-            for (Composition.PrimitiveEffect effect : mPrimitiveEffects) {
-                Composition.checkPrimitive(effect.id);
-                Preconditions.checkArgumentInRange(
-                        effect.scale, 0.0f, 1.0f, "scale");
-                Preconditions.checkArgumentNonNegative(effect.delay,
+            final int primitiveCount = mPrimitiveEffects.size();
+            for (int i = 0; i < primitiveCount; i++) {
+                Composition.PrimitiveEffect primitive = mPrimitiveEffects.get(i);
+                Composition.checkPrimitive(primitive.id);
+                Preconditions.checkArgumentInRange(primitive.scale, 0.0f, 1.0f, "scale");
+                Preconditions.checkArgumentNonNegative(primitive.delay,
                         "Primitive delay must be zero or positive");
             }
         }
diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java
index c357414..1d56e17 100644
--- a/core/tests/coretests/src/android/os/VibrationEffectTest.java
+++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java
@@ -39,6 +39,7 @@
 
 import com.android.internal.R;
 
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -168,15 +169,30 @@
     }
 
     @Test
-    public void testScalePrebaked_ignoresScaleAndReturnsSameEffect() {
-        VibrationEffect initial = VibrationEffect.get(VibrationEffect.RINGTONES[1]);
-        assertSame(initial, initial.scale(0.5f));
+    public void testScalePrebaked_scalesFallbackEffect() {
+        VibrationEffect.Prebaked prebaked =
+                (VibrationEffect.Prebaked) VibrationEffect.get(VibrationEffect.RINGTONES[1]);
+        assertSame(prebaked, prebaked.scale(0.5f));
+
+        prebaked = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                VibrationEffect.EFFECT_STRENGTH_MEDIUM, TEST_ONE_SHOT);
+        VibrationEffect.OneShot scaledFallback =
+                (VibrationEffect.OneShot) prebaked.scale(0.5f).getFallbackEffect();
+        assertEquals(34, scaledFallback.getAmplitude(), AMPLITUDE_SCALE_TOLERANCE);
     }
 
     @Test
-    public void testResolvePrebaked_ignoresDefaultAmplitudeAndReturnsSameEffect() {
-        VibrationEffect initial = VibrationEffect.get(VibrationEffect.RINGTONES[1]);
-        assertSame(initial, initial.resolve(1000));
+    public void testResolvePrebaked_resolvesFallbackEffectIfSet() {
+        VibrationEffect.Prebaked prebaked =
+                (VibrationEffect.Prebaked) VibrationEffect.get(VibrationEffect.RINGTONES[1]);
+        assertSame(prebaked, prebaked.resolve(1000));
+
+        prebaked = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                VibrationEffect.EFFECT_STRENGTH_MEDIUM,
+                VibrationEffect.createOneShot(1, VibrationEffect.DEFAULT_AMPLITUDE));
+        VibrationEffect.OneShot resolvedFallback =
+                (VibrationEffect.OneShot) prebaked.resolve(10).getFallbackEffect();
+        assertEquals(10, resolvedFallback.getAmplitude());
     }
 
     @Test
@@ -352,6 +368,36 @@
                 INTENSITY_SCALE_TOLERANCE);
     }
 
+    @Test
+    public void getEffectStrength_returnsValueFromConstructor() {
+        VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                VibrationEffect.EFFECT_STRENGTH_LIGHT, null);
+        Assert.assertEquals(VibrationEffect.EFFECT_STRENGTH_LIGHT, effect.getEffectStrength());
+    }
+
+    @Test
+    public void getFallbackEffect_withFallbackDisabled_isNull() {
+        VibrationEffect fallback = VibrationEffect.createOneShot(100, 100);
+        VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                false, VibrationEffect.EFFECT_STRENGTH_LIGHT);
+        Assert.assertNull(effect.getFallbackEffect());
+    }
+
+    @Test
+    public void getFallbackEffect_withoutEffectSet_isNull() {
+        VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                true, VibrationEffect.EFFECT_STRENGTH_LIGHT);
+        Assert.assertNull(effect.getFallbackEffect());
+    }
+
+    @Test
+    public void getFallbackEffect_withFallback_returnsValueFromConstructor() {
+        VibrationEffect fallback = VibrationEffect.createOneShot(100, 100);
+        VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                VibrationEffect.EFFECT_STRENGTH_LIGHT, fallback);
+        Assert.assertEquals(fallback, effect.getFallbackEffect());
+    }
+
     private Resources mockRingtoneResources() {
         return mockRingtoneResources(new String[] {
                 RINGTONE_URI_1,
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index 6a9715e..2c83da5 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -28,6 +28,7 @@
 import android.hardware.vibrator.IVibrator;
 import android.os.BatteryStats;
 import android.os.Binder;
+import android.os.CombinedVibrationEffect;
 import android.os.ExternalVibration;
 import android.os.Handler;
 import android.os.IBinder;
@@ -37,18 +38,15 @@
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.Process;
-import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.ShellCallback;
 import android.os.ShellCommand;
-import android.os.SystemClock;
 import android.os.Trace;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorInfo;
-import android.os.WorkSource;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
@@ -56,11 +54,11 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.util.DumpUtils;
-import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.vibrator.InputDeviceDelegate;
 import com.android.server.vibrator.Vibration;
 import com.android.server.vibrator.VibrationScaler;
 import com.android.server.vibrator.VibrationSettings;
+import com.android.server.vibrator.VibrationThread;
 import com.android.server.vibrator.VibratorController;
 import com.android.server.vibrator.VibratorController.OnVibrationCompleteListener;
 
@@ -90,10 +88,10 @@
     private final LinkedList<Vibration.DebugInfo> mPreviousExternalVibrations;
     private final LinkedList<Vibration.DebugInfo> mPreviousVibrations;
     private final int mPreviousVibrationsLimit;
-    private final WorkSource mTmpWorkSource = new WorkSource();
     private final Handler mH;
     private final Object mLock = new Object();
     private final VibratorController mVibratorController;
+    private final VibrationCallbacks mVibrationCallbacks = new VibrationCallbacks();
 
     private final Context mContext;
     private final PowerManager.WakeLock mWakeLock;
@@ -104,16 +102,40 @@
     private VibrationScaler mVibrationScaler;
     private InputDeviceDelegate mInputDeviceDelegate;
 
-    private volatile VibrateWaveformThread mThread;
+    private volatile VibrationThread mThread;
 
     @GuardedBy("mLock")
     private Vibration mCurrentVibration;
-    @GuardedBy("mLock")
-    private VibrationDeathRecipient mCurrentVibrationDeathRecipient;
     private int mCurVibUid = -1;
     private ExternalVibrationHolder mCurrentExternalVibration;
 
     /**
+     * Implementation of {@link VibrationThread.VibrationCallbacks} that reports finished
+     * vibrations.
+     */
+    private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks {
+
+        @Override
+        public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) {
+        }
+
+        @Override
+        public void triggerSyncedVibration(long vibrationId) {
+        }
+
+        @Override
+        public void onVibrationEnded(long vibrationId, Vibration.Status status) {
+            if (DEBUG) {
+                Slog.d(TAG, "Vibration thread finished with status " + status);
+            }
+            synchronized (mLock) {
+                mThread = null;
+                reportFinishVibrationLocked(status);
+            }
+        }
+    }
+
+    /**
      * Implementation of {@link OnVibrationCompleteListener} with a weak reference to this service.
      */
     private static final class VibrationCompleteListener implements OnVibrationCompleteListener {
@@ -127,41 +149,11 @@
         public void onComplete(int vibratorId, long vibrationId) {
             VibratorService service = mServiceRef.get();
             if (service != null) {
-                service.onVibrationComplete(vibrationId);
+                service.onVibrationComplete(vibratorId, vibrationId);
             }
         }
     }
 
-    /** Death recipient to bind {@link Vibration}. */
-    private final class VibrationDeathRecipient implements IBinder.DeathRecipient {
-
-        private final Vibration mVibration;
-
-        private VibrationDeathRecipient(Vibration vibration) {
-            mVibration = vibration;
-        }
-
-        @Override
-        public void binderDied() {
-            synchronized (mLock) {
-                if (mVibration == mCurrentVibration) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "Vibration finished because binder died, cleaning up");
-                    }
-                    doCancelVibrateLocked(Vibration.Status.CANCELLED);
-                }
-            }
-        }
-
-        private void linkToDeath() throws RemoteException {
-            mVibration.token.linkToDeath(this, 0);
-        }
-
-        private void unlinkToDeath() {
-            mVibration.token.unlinkToDeath(this, 0);
-        }
-    }
-
     /** Holder for a {@link ExternalVibration}. */
     private final class ExternalVibrationHolder {
 
@@ -262,13 +254,20 @@
 
     /** Callback for when vibration is complete, to be called by native. */
     @VisibleForTesting
-    public void onVibrationComplete(long vibrationId) {
+    public void onVibrationComplete(int vibratorId, long vibrationId) {
         synchronized (mLock) {
             if (mCurrentVibration != null && mCurrentVibration.id == vibrationId) {
                 if (DEBUG) {
-                    Slog.d(TAG, "Vibration finished by callback, cleaning up");
+                    Slog.d(TAG, "Vibration onComplete callback, notifying VibrationThread");
                 }
-                doCancelVibrateLocked(Vibration.Status.FINISHED);
+                if (mThread != null) {
+                    // Let the thread playing the vibration handle the callback, since it might be
+                    // expecting the vibrator to turn off multiple times during a single vibration.
+                    mThread.vibratorComplete(vibratorId);
+                } else {
+                    // No vibration is playing in the thread, but clean up service just in case.
+                    doCancelVibrateLocked(Vibration.Status.FINISHED);
+                }
             }
         }
     }
@@ -354,6 +353,17 @@
         return true;
     }
 
+    private VibrationEffect fixupVibrationEffect(VibrationEffect effect) {
+        if (effect instanceof VibrationEffect.Prebaked
+                && ((VibrationEffect.Prebaked) effect).shouldFallback()) {
+            VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+            VibrationEffect fallback = mVibrationSettings.getFallbackEffect(prebaked.getId());
+            return new VibrationEffect.Prebaked(prebaked.getId(), prebaked.getEffectStrength(),
+                    fallback);
+        }
+        return effect;
+    }
+
     private VibrationAttributes fixupVibrationAttributes(VibrationAttributes attrs) {
         if (attrs == null) {
             attrs = DEFAULT_ATTRIBUTES;
@@ -388,16 +398,16 @@
             if (!verifyVibrationEffect(effect)) {
                 return;
             }
-
+            effect = fixupVibrationEffect(effect);
             attrs = fixupVibrationAttributes(attrs);
-            Vibration vib = new Vibration(token, mNextVibrationId.getAndIncrement(), effect, attrs,
-                    uid, opPkg, reason);
+            Vibration vib = new Vibration(token, mNextVibrationId.getAndIncrement(),
+                    CombinedVibrationEffect.createSynced(effect), attrs, uid, opPkg, reason);
 
             // If our current vibration is longer than the new vibration and is the same amplitude,
             // then just let the current one finish.
             synchronized (mLock) {
                 VibrationEffect currentEffect =
-                        mCurrentVibration == null ? null : mCurrentVibration.getEffect();
+                        mCurrentVibration == null ? null : getEffect(mCurrentVibration);
                 if (effect instanceof VibrationEffect.OneShot
                         && currentEffect instanceof VibrationEffect.OneShot) {
                     VibrationEffect.OneShot newOneShot = (VibrationEffect.OneShot) effect;
@@ -446,7 +456,6 @@
                     endVibrationLocked(vib, Vibration.Status.IGNORED_BACKGROUND);
                     return;
                 }
-                linkVibrationLocked(vib);
                 final long ident = Binder.clearCallingIdentity();
                 try {
                     doCancelVibrateLocked(Vibration.Status.CANCELLED);
@@ -474,6 +483,10 @@
         return effect.getDuration() == Long.MAX_VALUE;
     }
 
+    private static <T extends VibrationEffect> T getEffect(Vibration vib) {
+        return (T) ((CombinedVibrationEffect.Mono) vib.getEffect()).getEffect();
+    }
+
     private void endVibrationLocked(Vibration vib, Vibration.Status status) {
         final LinkedList<Vibration.DebugInfo> previousVibrations;
         switch (vib.attrs.getUsage()) {
@@ -527,7 +540,6 @@
 
     @GuardedBy("mLock")
     private void doCancelVibrateLocked(Vibration.Status status) {
-        Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doCancelVibrateLocked");
         try {
             if (mThread != null) {
@@ -547,18 +559,6 @@
         }
     }
 
-    // Callback for whenever the current vibration has finished played out
-    public void onVibrationFinished() {
-        if (DEBUG) {
-            Slog.d(TAG, "Vibration finished, cleaning up");
-        }
-        synchronized (mLock) {
-            // Make sure the vibration is really done. This also reports that the vibration is
-            // finished.
-            doCancelVibrateLocked(Vibration.Status.FINISHED);
-        }
-    }
-
     @GuardedBy("mLock")
     private void startVibrationLocked(final Vibration vib) {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
@@ -579,24 +579,20 @@
         try {
             // Set current vibration before starting it, so callback will work.
             mCurrentVibration = vib;
-            VibrationEffect effect = vib.getEffect();
-            if (effect instanceof VibrationEffect.OneShot) {
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
-                doVibratorOn(vib);
-            } else if (effect instanceof VibrationEffect.Waveform) {
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
-                doVibratorWaveformEffectLocked(vib);
-            } else if (effect instanceof VibrationEffect.Prebaked) {
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
-                doVibratorPrebakedEffectLocked(vib);
-            } else if (effect instanceof VibrationEffect.Composed) {
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
-                doVibratorComposedEffectLocked(vib);
-            } else {
-                Slog.e(TAG, "Unknown vibration type, ignoring");
-                endVibrationLocked(vib, Vibration.Status.IGNORED_UNKNOWN_VIBRATION);
-                // The set current vibration is not actually playing, so drop it.
+            VibrationEffect effect = getEffect(vib);
+            Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+            boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
+                    vib.uid, vib.opPkg, vib.getEffect(), vib.reason, vib.attrs);
+            if (inputDevicesAvailable) {
+                // The set current vibration is no longer being played by this service, so drop it.
                 mCurrentVibration = null;
+                endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
+            } else {
+                // mThread better be null here. doCancelVibrate should always be
+                // called before startVibrationInnerLocked
+                mThread = new VibrationThread(vib, mVibratorController, mWakeLock,
+                        mBatteryStatsService, mVibrationCallbacks);
+                mThread.start();
             }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
@@ -667,13 +663,13 @@
 
     @GuardedBy("mLock")
     private void reportFinishVibrationLocked(Vibration.Status status) {
+        Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "reportFinishVibrationLocked");
         try {
             if (mCurrentVibration != null) {
                 endVibrationLocked(mCurrentVibration, status);
                 mAppOps.finishOp(AppOpsManager.OP_VIBRATE, mCurrentVibration.uid,
                         mCurrentVibration.opPkg);
-                unlinkVibrationLocked();
                 mCurrentVibration = null;
             }
         } finally {
@@ -681,30 +677,6 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void linkVibrationLocked(Vibration vib) {
-        // Unlink previously linked vibration, if any.
-        unlinkVibrationLocked();
-        // Only link against waveforms since they potentially don't have a finish if
-        // they're repeating. Let other effects just play out until they're done.
-        if (vib.getEffect() instanceof VibrationEffect.Waveform) {
-            try {
-                mCurrentVibrationDeathRecipient = new VibrationDeathRecipient(vib);
-                mCurrentVibrationDeathRecipient.linkToDeath();
-            } catch (RemoteException e) {
-                return;
-            }
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void unlinkVibrationLocked() {
-        if (mCurrentVibrationDeathRecipient != null) {
-            mCurrentVibrationDeathRecipient.unlinkToDeath();
-            mCurrentVibrationDeathRecipient = null;
-        }
-    }
-
     private void updateVibrators() {
         synchronized (mLock) {
             mInputDeviceDelegate.updateInputDeviceVibrators(
@@ -715,40 +687,12 @@
         }
     }
 
-    private void doVibratorOn(Vibration vib) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorOn");
-        try {
-            final VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) vib.getEffect();
-            if (DEBUG) {
-                Slog.d(TAG, "Turning vibrator on for " + oneShot.getDuration() + " ms"
-                        + " with amplitude " + oneShot.getAmplitude() + ".");
-            }
-            boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
-                    vib.uid, vib.opPkg, oneShot, vib.reason, vib.attrs);
-            if (inputDevicesAvailable) {
-                // The set current vibration is no longer being played by this service, so drop it.
-                mCurrentVibration = null;
-                endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
-            } else {
-                noteVibratorOnLocked(vib.uid, oneShot.getDuration());
-                // Note: ordering is important here! Many haptic drivers will reset their
-                // amplitude when enabled, so we always have to enable first, then set the
-                // amplitude.
-                mVibratorController.on(oneShot.getDuration(), vib.id);
-                mVibratorController.setAmplitude(oneShot.getAmplitude());
-            }
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
-    }
-
     private void doVibratorOff() {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorOff");
         try {
             if (DEBUG) {
                 Slog.d(TAG, "Turning vibrator off.");
             }
-            noteVibratorOffLocked();
             boolean inputDevicesAvailable = mInputDeviceDelegate.cancelVibrateIfAvailable();
             if (!inputDevicesAvailable) {
                 mVibratorController.off();
@@ -758,95 +702,6 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void doVibratorWaveformEffectLocked(Vibration vib) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorWaveformEffectLocked");
-        try {
-            boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
-                    vib.uid, vib.opPkg, vib.getEffect(), vib.reason, vib.attrs);
-            if (inputDevicesAvailable) {
-                // The set current vibration is no longer being played by this service, so drop it.
-                mCurrentVibration = null;
-                endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
-            } else {
-                // mThread better be null here. doCancelVibrate should always be
-                // called before startNextVibrationLocked or startVibrationLocked.
-                mThread = new VibrateWaveformThread(vib);
-                mThread.start();
-            }
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void doVibratorPrebakedEffectLocked(Vibration vib) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorPrebakedEffectLocked");
-        try {
-            final VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) vib.getEffect();
-            // Input devices don't support prebaked effect, so skip trying it with them and allow
-            // fallback to be attempted.
-            if (!mInputDeviceDelegate.isAvailable()) {
-                long duration = mVibratorController.on(prebaked, vib.id);
-                if (duration > 0) {
-                    noteVibratorOnLocked(vib.uid, duration);
-                    return;
-                }
-            }
-            endVibrationLocked(vib, Vibration.Status.IGNORED_UNSUPPORTED);
-            // The set current vibration is not actually playing, so drop it.
-            mCurrentVibration = null;
-
-            if (!prebaked.shouldFallback()) {
-                return;
-            }
-            VibrationEffect effect = mVibrationSettings.getFallbackEffect(prebaked.getId());
-            if (effect == null) {
-                Slog.w(TAG, "Failed to play prebaked effect, no fallback");
-                return;
-            }
-            Vibration fallbackVib = new Vibration(vib.token, mNextVibrationId.getAndIncrement(),
-                    effect, vib.attrs, vib.uid, vib.opPkg, vib.reason + " (fallback)");
-            // Set current vibration before starting it, so callback will work.
-            mCurrentVibration = fallbackVib;
-            linkVibrationLocked(fallbackVib);
-            applyVibrationIntensityScalingLocked(fallbackVib);
-            startVibrationInnerLocked(fallbackVib);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void doVibratorComposedEffectLocked(Vibration vib) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorComposedEffectLocked");
-
-        try {
-            final VibrationEffect.Composed composed = (VibrationEffect.Composed) vib.getEffect();
-            boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
-                    vib.uid, vib.opPkg, composed, vib.reason, vib.attrs);
-            if (inputDevicesAvailable) {
-                // The set current vibration is no longer being played by this service, so drop it.
-                mCurrentVibration = null;
-                endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
-                return;
-            } else if (!mVibratorController.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
-                // The set current vibration is not actually playing, so drop it.
-                mCurrentVibration = null;
-                endVibrationLocked(vib, Vibration.Status.IGNORED_UNSUPPORTED);
-                return;
-            }
-
-            mVibratorController.on(composed, vib.id);
-
-            // Composed effects don't actually give us an estimated duration, so we just guess here.
-            noteVibratorOnLocked(vib.uid, 10 * composed.getPrimitiveEffects().size());
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
-
-    }
-
     private boolean isSystemHapticFeedback(Vibration vib) {
         if (vib.attrs.getUsage() != VibrationAttributes.USAGE_TOUCH) {
             return false;
@@ -854,27 +709,6 @@
         return vib.uid == Process.SYSTEM_UID || vib.uid == 0 || mSystemUiPackage.equals(vib.opPkg);
     }
 
-    private void noteVibratorOnLocked(int uid, long millis) {
-        try {
-            mBatteryStatsService.noteVibratorOn(uid, millis);
-            FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, uid, null,
-                    FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON, millis);
-            mCurVibUid = uid;
-        } catch (RemoteException e) {
-        }
-    }
-
-    private void noteVibratorOffLocked() {
-        if (mCurVibUid >= 0) {
-            try {
-                mBatteryStatsService.noteVibratorOff(mCurVibUid);
-                FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
-                        mCurVibUid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF, 0);
-            } catch (RemoteException e) { }
-            mCurVibUid = -1;
-        }
-    }
-
     private void dumpInternal(PrintWriter pw) {
         pw.println("Vibrator Service:");
         synchronized (mLock) {
@@ -972,156 +806,6 @@
         proto.flush();
     }
 
-    /** Thread that plays a single {@link VibrationEffect.Waveform}. */
-    private class VibrateWaveformThread extends Thread {
-        private final VibrationEffect.Waveform mWaveform;
-        private final Vibration mVibration;
-
-        private boolean mForceStop;
-
-        VibrateWaveformThread(Vibration vib) {
-            mWaveform = (VibrationEffect.Waveform) vib.getEffect();
-            mVibration = new Vibration(vib.token, /* id= */ 0, /* effect= */ null, vib.attrs,
-                    vib.uid, vib.opPkg, vib.reason);
-            mTmpWorkSource.set(vib.uid);
-            mWakeLock.setWorkSource(mTmpWorkSource);
-        }
-
-        private void delayLocked(long wakeUpTime) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "delayLocked");
-            try {
-                long durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
-                while (durationRemaining > 0) {
-                    try {
-                        this.wait(durationRemaining);
-                    } catch (InterruptedException e) {
-                    }
-                    if (mForceStop) {
-                        break;
-                    }
-                    durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
-                }
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-
-        public void run() {
-            Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
-            mWakeLock.acquire();
-            try {
-                boolean finished = playWaveform();
-                if (finished) {
-                    onVibrationFinished();
-                }
-            } finally {
-                mWakeLock.release();
-            }
-        }
-
-        /**
-         * Play the waveform.
-         *
-         * @return true if it finished naturally, false otherwise (e.g. it was canceled).
-         */
-        public boolean playWaveform() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playWaveform");
-            try {
-                synchronized (this) {
-                    final long[] timings = mWaveform.getTimings();
-                    final int[] amplitudes = mWaveform.getAmplitudes();
-                    final int len = timings.length;
-                    final int repeat = mWaveform.getRepeatIndex();
-
-                    int index = 0;
-                    long nextStepStartTime = SystemClock.uptimeMillis();
-                    long nextVibratorStopTime = 0;
-                    while (!mForceStop) {
-                        if (index < len) {
-                            final int amplitude = amplitudes[index];
-                            final long duration = timings[index++];
-                            if (duration <= 0) {
-                                continue;
-                            }
-                            if (amplitude != 0) {
-                                long now = SystemClock.uptimeMillis();
-                                if (nextVibratorStopTime <= now) {
-                                    // Telling the vibrator to start multiple times usually causes
-                                    // effects to feel "choppy" because the motor resets at every on
-                                    // command.  Instead we figure out how long our next "on" period
-                                    // is going to be, tell the motor to stay on for the full
-                                    // duration, and then wake up to change the amplitude at the
-                                    // appropriate intervals.
-                                    long onDuration = getTotalOnDuration(
-                                            timings, amplitudes, index - 1, repeat);
-                                    mVibration.updateEffect(
-                                            VibrationEffect.createOneShot(onDuration, amplitude));
-                                    doVibratorOn(mVibration);
-                                    nextVibratorStopTime = now + onDuration;
-                                } else {
-                                    // Vibrator is already ON, so just change its amplitude.
-                                    mVibratorController.setAmplitude(amplitude);
-                                }
-                            } else {
-                                // Previous vibration should have already finished, but we make sure
-                                // the vibrator will be off for the next step when amplitude is 0.
-                                doVibratorOff();
-                            }
-
-                            // We wait until the time this waveform step was supposed to end,
-                            // calculated from the time it was supposed to start. All start times
-                            // are calculated from the waveform original start time by adding the
-                            // input durations. Any scheduling or processing delay should not affect
-                            // this step's perceived total duration. They will be amortized here.
-                            nextStepStartTime += duration;
-                            delayLocked(nextStepStartTime);
-                        } else if (repeat < 0) {
-                            break;
-                        } else {
-                            index = repeat;
-                        }
-                    }
-                    return !mForceStop;
-                }
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-
-        public void cancel() {
-            synchronized (this) {
-                mThread.mForceStop = true;
-                mThread.notify();
-            }
-        }
-
-        /**
-         * Get the duration the vibrator will be on starting at startIndex until the next time it's
-         * off.
-         */
-        private long getTotalOnDuration(
-                long[] timings, int[] amplitudes, int startIndex, int repeatIndex) {
-            int i = startIndex;
-            long timing = 0;
-            while (amplitudes[i] != 0) {
-                timing += timings[i++];
-                if (i >= timings.length) {
-                    if (repeatIndex >= 0) {
-                        i = repeatIndex;
-                        // prevent infinite loop
-                        repeatIndex = -1;
-                    } else {
-                        break;
-                    }
-                }
-                if (i == startIndex) {
-                    return 1000;
-                }
-            }
-            return timing;
-        }
-    }
-
     /** Point of injection for test dependencies */
     @VisibleForTesting
     static class Injector {
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 3e0d040..23c70ee 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1856,6 +1856,13 @@
         }
 
         VibrationInfo(VibrationEffect effect) {
+            // First replace prebaked effects with its fallback, if any available.
+            if (effect instanceof VibrationEffect.Prebaked) {
+                VibrationEffect fallback = ((VibrationEffect.Prebaked) effect).getFallbackEffect();
+                if (fallback != null) {
+                    effect = fallback;
+                }
+            }
             if (effect instanceof VibrationEffect.OneShot) {
                 VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect;
                 mPattern = new long[] { 0, oneShot.getDuration() };
@@ -1882,8 +1889,7 @@
                     throw new ArrayIndexOutOfBoundsException();
                 }
             } else {
-                // TODO: Add support for prebaked effects
-                Slog.w(TAG, "Pre-baked effects aren't supported on input devices");
+                Slog.w(TAG, "Pre-baked and composed effects aren't supported on input devices");
             }
         }
     }
diff --git a/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java b/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java
index edbc058..3968723 100644
--- a/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java
+++ b/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.hardware.input.InputManager;
+import android.os.CombinedVibrationEffect;
 import android.os.Handler;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
@@ -84,11 +85,21 @@
      *
      * @return {@link #isAvailable()}
      */
-    public boolean vibrateIfAvailable(int uid, String opPkg, VibrationEffect effect,
+    public boolean vibrateIfAvailable(int uid, String opPkg, CombinedVibrationEffect effect,
             String reason, VibrationAttributes attrs) {
         synchronized (mLock) {
-            for (int i = 0; i < mInputDeviceVibrators.size(); i++) {
-                mInputDeviceVibrators.valueAt(i).vibrate(uid, opPkg, effect, reason, attrs);
+            // TODO(b/159207608): Pass on the combined vibration once InputManager is merged
+            if (effect instanceof CombinedVibrationEffect.Mono) {
+                VibrationEffect e = ((CombinedVibrationEffect.Mono) effect).getEffect();
+                if (e instanceof VibrationEffect.Prebaked) {
+                    VibrationEffect fallback = ((VibrationEffect.Prebaked) e).getFallbackEffect();
+                    if (fallback != null) {
+                        e = fallback;
+                    }
+                }
+                for (int i = 0; i < mInputDeviceVibrators.size(); i++) {
+                    mInputDeviceVibrators.valueAt(i).vibrate(uid, opPkg, e, reason, attrs);
+                }
             }
             return mInputDeviceVibrators.size() > 0;
         }
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index b0266d0..fe3b03ab 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.os.CombinedVibrationEffect;
 import android.os.IBinder;
 import android.os.SystemClock;
 import android.os.VibrationAttributes;
@@ -72,14 +73,14 @@
 
     /** The actual effect to be played. */
     @Nullable
-    private VibrationEffect mEffect;
+    private CombinedVibrationEffect mEffect;
 
     /**
      * The original effect that was requested. Typically these two things differ because the effect
      * was scaled based on the users vibration intensity settings.
      */
     @Nullable
-    private VibrationEffect mOriginalEffect;
+    private CombinedVibrationEffect mOriginalEffect;
 
     /**
      * Start/end times in unix epoch time. Only to be used for debugging purposes and to correlate
@@ -90,7 +91,7 @@
     private long mEndTimeDebug;
     private Status mStatus;
 
-    public Vibration(IBinder token, int id, VibrationEffect effect,
+    public Vibration(IBinder token, int id, CombinedVibrationEffect effect,
             VibrationAttributes attrs, int uid, String opPkg, String reason) {
         this.token = token;
         this.mEffect = effect;
@@ -124,7 +125,7 @@
      * Replace this vibration effect if given {@code scaledEffect} is different, preserving the
      * original one for debug purposes.
      */
-    public void updateEffect(@NonNull VibrationEffect newEffect) {
+    public void updateEffect(@NonNull CombinedVibrationEffect newEffect) {
         if (newEffect.equals(mEffect)) {
             return;
         }
@@ -139,7 +140,7 @@
 
     /** Return the effect that should be played by this vibration. */
     @Nullable
-    public VibrationEffect getEffect() {
+    public CombinedVibrationEffect getEffect() {
         return mEffect;
     }
 
@@ -154,8 +155,8 @@
     public static final class DebugInfo {
         private final long mStartTimeDebug;
         private final long mEndTimeDebug;
-        private final VibrationEffect mEffect;
-        private final VibrationEffect mOriginalEffect;
+        private final CombinedVibrationEffect mEffect;
+        private final CombinedVibrationEffect mOriginalEffect;
         private final float mScale;
         private final VibrationAttributes mAttrs;
         private final int mUid;
@@ -163,8 +164,8 @@
         private final String mReason;
         private final Status mStatus;
 
-        public DebugInfo(long startTimeDebug, long endTimeDebug, VibrationEffect effect,
-                VibrationEffect originalEffect, float scale, VibrationAttributes attrs,
+        public DebugInfo(long startTimeDebug, long endTimeDebug, CombinedVibrationEffect effect,
+                CombinedVibrationEffect originalEffect, float scale, VibrationAttributes attrs,
                 int uid, String opPkg, String reason, Status status) {
             mStartTimeDebug = startTimeDebug;
             mEndTimeDebug = endTimeDebug;
@@ -228,7 +229,22 @@
             proto.end(token);
         }
 
-        private void dumpEffect(ProtoOutputStream proto, long fieldId, VibrationEffect effect) {
+        private void dumpEffect(
+                ProtoOutputStream proto, long fieldId, CombinedVibrationEffect combinedEffect) {
+            VibrationEffect effect;
+            // TODO(b/177805090): add proper support for dumping combined effects to proto
+            if (combinedEffect instanceof CombinedVibrationEffect.Mono) {
+                effect = ((CombinedVibrationEffect.Mono) combinedEffect).getEffect();
+            } else if (combinedEffect instanceof CombinedVibrationEffect.Stereo) {
+                effect = ((CombinedVibrationEffect.Stereo) combinedEffect).getEffects().valueAt(0);
+            } else if (combinedEffect instanceof CombinedVibrationEffect.Sequential) {
+                dumpEffect(proto, fieldId,
+                        ((CombinedVibrationEffect.Sequential) combinedEffect).getEffects().get(0));
+                return;
+            } else {
+                // Unknown combined effect, skip dump.
+                return;
+            }
             final long token = proto.start(fieldId);
             if (effect instanceof VibrationEffect.OneShot) {
                 dumpEffect(proto, VibrationEffectProto.ONESHOT, (VibrationEffect.OneShot) effect);
diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index 5f7e47d..0fa4fe1 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -18,12 +18,16 @@
 
 import android.content.Context;
 import android.hardware.vibrator.V1_0.EffectStrength;
+import android.os.CombinedVibrationEffect;
 import android.os.IExternalVibratorService;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.util.Slog;
 import android.util.SparseArray;
 
+import java.util.List;
+import java.util.Objects;
+
 /** Controls vibration scaling. */
 // TODO(b/159207608): Make this package-private once vibrator services are moved to this package
 public final class VibrationScaler {
@@ -87,6 +91,43 @@
     }
 
     /**
+     * Scale a {@link CombinedVibrationEffect} based on the given usage hint for this vibration.
+     *
+     * @param combinedEffect the effect to be scaled
+     * @param usageHint      one of VibrationAttributes.USAGE_*
+     * @return The same given effect, if no changes were made, or a new
+     * {@link CombinedVibrationEffect} with resolved and scaled amplitude
+     */
+    public <T extends CombinedVibrationEffect> T scale(CombinedVibrationEffect combinedEffect,
+            int usageHint) {
+        if (combinedEffect instanceof CombinedVibrationEffect.Mono) {
+            VibrationEffect effect = ((CombinedVibrationEffect.Mono) combinedEffect).getEffect();
+            return (T) CombinedVibrationEffect.createSynced(scale(effect, usageHint));
+        } else if (combinedEffect instanceof CombinedVibrationEffect.Stereo) {
+            SparseArray<VibrationEffect> effects =
+                    ((CombinedVibrationEffect.Stereo) combinedEffect).getEffects();
+            CombinedVibrationEffect.SyncedCombination combination =
+                    CombinedVibrationEffect.startSynced();
+            for (int i = 0; i < effects.size(); i++) {
+                combination.addVibrator(effects.keyAt(i), scale(effects.valueAt(i), usageHint));
+            }
+            return (T) combination.combine();
+        } else if (combinedEffect instanceof CombinedVibrationEffect.Sequential) {
+            List<CombinedVibrationEffect> effects =
+                    ((CombinedVibrationEffect.Sequential) combinedEffect).getEffects();
+            CombinedVibrationEffect.SequentialCombination combination =
+                    CombinedVibrationEffect.startSequential();
+            for (CombinedVibrationEffect effect : effects) {
+                combination.addNext(scale(effect, usageHint));
+            }
+            return (T) combination.combine();
+        } else {
+            // Unknown combination, return same effect.
+            return (T) combinedEffect;
+        }
+    }
+
+    /**
      * Scale a {@link VibrationEffect} based on the given usage hint for this vibration.
      *
      * @param effect    the effect to be scaled
@@ -100,13 +141,23 @@
             int intensity = mSettingsController.getCurrentIntensity(usageHint);
             int newStrength = intensityToEffectStrength(intensity);
             VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+            int strength = prebaked.getEffectStrength();
+            VibrationEffect fallback = prebaked.getFallbackEffect();
 
-            if (prebaked.getEffectStrength() == newStrength) {
+            if (fallback != null) {
+                VibrationEffect scaledFallback = scale(fallback, usageHint);
+                if (strength == newStrength && Objects.equals(fallback, scaledFallback)) {
+                    return (T) prebaked;
+                }
+
+                return (T) new VibrationEffect.Prebaked(prebaked.getId(), newStrength,
+                        scaledFallback);
+            } else if (strength == newStrength) {
                 return (T) prebaked;
+            } else {
+                return (T) new VibrationEffect.Prebaked(prebaked.getId(), prebaked.shouldFallback(),
+                        newStrength);
             }
-
-            return (T) new VibrationEffect.Prebaked(
-                    prebaked.getId(), prebaked.shouldFallback(), newStrength);
         }
 
         effect = effect.resolve(mDefaultVibrationAmplitude);
@@ -124,8 +175,6 @@
         return effect.scale(scale.factor);
     }
 
-
-
     /** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */
     private static int intensityToEffectStrength(int intensity) {
         switch (intensity) {
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
new file mode 100644
index 0000000..a4d888b
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -0,0 +1,791 @@
+/*
+ * Copyright (C) 2020 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.Nullable;
+import android.os.CombinedVibrationEffect;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.WorkSource;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.util.FrameworkStatsLog;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Plays a {@link Vibration} in dedicated thread. */
+// TODO(b/159207608): Make this package-private once vibrator services are moved to this package
+public final class VibrationThread extends Thread implements IBinder.DeathRecipient {
+    private static final String TAG = "VibrationThread";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Extra timeout added to the end of each synced vibration step as a timeout for the callback
+     * wait, to ensure it finishes even when callbacks from individual vibrators are lost.
+     */
+    private static final long CALLBACKS_EXTRA_TIMEOUT = 100;
+
+    /** Callbacks for playing a {@link Vibration}. */
+    public interface VibrationCallbacks {
+
+        /**
+         * Callback triggered before starting a synchronized vibration step. This will be called
+         * with {@code requiredCapabilities = 0} if no synchronization is required.
+         *
+         * @param requiredCapabilities The required syncing capabilities for this preparation step.
+         *                             Expects a combination of values from
+         *                             IVibratorManager.CAP_PREPARE_* and
+         *                             IVibratorManager.CAP_MIXED_TRIGGER_*.
+         * @param vibratorIds          The id of the vibrators to be prepared.
+         */
+        void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds);
+
+        /** Callback triggered after synchronized vibrations were prepared. */
+        void triggerSyncedVibration(long vibrationId);
+
+        /** Callback triggered when vibration thread is complete. */
+        void onVibrationEnded(long vibrationId, Vibration.Status status);
+    }
+
+    private final Object mLock = new Object();
+    private final WorkSource mWorkSource = new WorkSource();
+    private final PowerManager.WakeLock mWakeLock;
+    private final IBatteryStats mBatteryStatsService;
+    private final Vibration mVibration;
+    private final VibrationCallbacks mCallbacks;
+    private final SparseArray<VibratorController> mVibrators;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private VibrateStep mCurrentVibrateStep;
+    @GuardedBy("this")
+    private boolean mForceStop;
+
+    // TODO(b/159207608): Remove this constructor once VibratorService is removed
+    public VibrationThread(Vibration vib, VibratorController vibrator,
+            PowerManager.WakeLock wakeLock, IBatteryStats batteryStatsService,
+            VibrationCallbacks callbacks) {
+        this(vib, toSparseArray(vibrator), wakeLock, batteryStatsService, callbacks);
+    }
+
+    public VibrationThread(Vibration vib, SparseArray<VibratorController> availableVibrators,
+            PowerManager.WakeLock wakeLock, IBatteryStats batteryStatsService,
+            VibrationCallbacks callbacks) {
+        mVibration = vib;
+        mCallbacks = callbacks;
+        mWakeLock = wakeLock;
+        mWorkSource.set(vib.uid);
+        mWakeLock.setWorkSource(mWorkSource);
+        mBatteryStatsService = batteryStatsService;
+
+        CombinedVibrationEffect effect = vib.getEffect();
+        mVibrators = new SparseArray<>();
+        for (int i = 0; i < availableVibrators.size(); i++) {
+            if (effect.hasVibrator(availableVibrators.keyAt(i))) {
+                mVibrators.put(availableVibrators.keyAt(i), availableVibrators.valueAt(i));
+            }
+        }
+    }
+
+    @Override
+    public void binderDied() {
+        cancel();
+    }
+
+    @Override
+    public void run() {
+        Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+        mWakeLock.acquire();
+        try {
+            mVibration.token.linkToDeath(this, 0);
+            Vibration.Status status = playVibration();
+            mCallbacks.onVibrationEnded(mVibration.id, status);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error linking vibration to token death", e);
+        } finally {
+            mVibration.token.unlinkToDeath(this, 0);
+            mWakeLock.release();
+        }
+    }
+
+    /** Cancel current vibration and shuts down the thread gracefully. */
+    public void cancel() {
+        synchronized (this) {
+            mForceStop = true;
+            notify();
+        }
+    }
+
+    /** Notify current vibration that a step has completed on given vibrator. */
+    public void vibratorComplete(int vibratorId) {
+        synchronized (mLock) {
+            if (mCurrentVibrateStep != null) {
+                mCurrentVibrateStep.vibratorComplete(vibratorId);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    SparseArray<VibratorController> getVibrators() {
+        return mVibrators;
+    }
+
+    private Vibration.Status playVibration() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
+        try {
+            List<Step> steps = generateSteps(mVibration.getEffect());
+            if (steps.isEmpty()) {
+                // No vibrator matching any incoming vibration effect.
+                return Vibration.Status.IGNORED;
+            }
+            Vibration.Status status = Vibration.Status.FINISHED;
+            final int stepCount = steps.size();
+            for (int i = 0; i < stepCount; i++) {
+                Step step = steps.get(i);
+                synchronized (mLock) {
+                    if (step instanceof VibrateStep) {
+                        mCurrentVibrateStep = (VibrateStep) step;
+                    } else {
+                        mCurrentVibrateStep = null;
+                    }
+                }
+                status = step.play();
+                if (status != Vibration.Status.FINISHED) {
+                    // This step was ignored by the vibrators, probably effects were unsupported.
+                    break;
+                }
+                if (mForceStop) {
+                    break;
+                }
+            }
+            if (mForceStop) {
+                return Vibration.Status.CANCELLED;
+            }
+            return status;
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    private List<Step> generateSteps(CombinedVibrationEffect effect) {
+        if (effect instanceof CombinedVibrationEffect.Sequential) {
+            CombinedVibrationEffect.Sequential sequential =
+                    (CombinedVibrationEffect.Sequential) effect;
+            List<Step> steps = new ArrayList<>();
+            final int sequentialEffectCount = sequential.getEffects().size();
+            for (int i = 0; i < sequentialEffectCount; i++) {
+                int delay = sequential.getDelays().get(i);
+                if (delay > 0) {
+                    steps.add(new DelayStep(delay));
+                }
+                steps.addAll(generateSteps(sequential.getEffects().get(i)));
+            }
+            final int stepCount = steps.size();
+            for (int i = 0; i < stepCount; i++) {
+                if (steps.get(i) instanceof VibrateStep) {
+                    return steps;
+                }
+            }
+            // No valid vibrate step was generated, ignore effect completely.
+            return Lists.newArrayList();
+        }
+        VibrateStep vibrateStep = null;
+        if (effect instanceof CombinedVibrationEffect.Mono) {
+            vibrateStep = createVibrateStep(mapToAvailableVibrators(
+                    ((CombinedVibrationEffect.Mono) effect).getEffect()));
+        } else if (effect instanceof CombinedVibrationEffect.Stereo) {
+            vibrateStep = createVibrateStep(filterByAvailableVibrators(
+                    ((CombinedVibrationEffect.Stereo) effect).getEffects()));
+        }
+        return vibrateStep == null ? Lists.newArrayList() : Lists.newArrayList(vibrateStep);
+    }
+
+    @Nullable
+    private VibrateStep createVibrateStep(SparseArray<VibrationEffect> effects) {
+        if (effects.size() == 0) {
+            return null;
+        }
+        if (effects.size() == 1) {
+            // Create simplified step that handles a single vibrator.
+            return new SingleVibrateStep(mVibrators.get(effects.keyAt(0)), effects.valueAt(0));
+        }
+        return new SyncedVibrateStep(effects);
+    }
+
+    private SparseArray<VibrationEffect> mapToAvailableVibrators(VibrationEffect effect) {
+        SparseArray<VibrationEffect> mappedEffects = new SparseArray<>(mVibrators.size());
+        for (int i = 0; i < mVibrators.size(); i++) {
+            mappedEffects.put(mVibrators.keyAt(i), effect);
+        }
+        return mappedEffects;
+    }
+
+    private SparseArray<VibrationEffect> filterByAvailableVibrators(
+            SparseArray<VibrationEffect> effects) {
+        SparseArray<VibrationEffect> filteredEffects = new SparseArray<>();
+        for (int i = 0; i < effects.size(); i++) {
+            if (mVibrators.contains(effects.keyAt(i))) {
+                filteredEffects.put(effects.keyAt(i), effects.valueAt(i));
+            }
+        }
+        return filteredEffects;
+    }
+
+    private static SparseArray<VibratorController> toSparseArray(VibratorController controller) {
+        SparseArray<VibratorController> array = new SparseArray<>(1);
+        array.put(controller.getVibratorInfo().getId(), controller);
+        return array;
+    }
+
+    /**
+     * Get the duration the vibrator will be on for given {@code waveform}, starting at {@code
+     * startIndex} until the next time it's vibrating amplitude is zero.
+     */
+    private static long getVibratorOnDuration(VibrationEffect.Waveform waveform, int startIndex) {
+        long[] timings = waveform.getTimings();
+        int[] amplitudes = waveform.getAmplitudes();
+        int repeatIndex = waveform.getRepeatIndex();
+        int i = startIndex;
+        long timing = 0;
+        while (timings[i] == 0 || amplitudes[i] != 0) {
+            timing += timings[i++];
+            if (i >= timings.length) {
+                if (repeatIndex >= 0) {
+                    i = repeatIndex;
+                    // prevent infinite loop
+                    repeatIndex = -1;
+                } else {
+                    break;
+                }
+            }
+            if (i == startIndex) {
+                return 1000;
+            }
+        }
+        return timing;
+    }
+
+    /**
+     * Sleeps until given {@code wakeUpTime}.
+     *
+     * <p>This stops immediately when {@link #cancel()} is called.
+     */
+    private void waitUntil(long wakeUpTime) {
+        synchronized (this) {
+            long durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+            while (durationRemaining > 0) {
+                try {
+                    VibrationThread.this.wait(durationRemaining);
+                } catch (InterruptedException e) {
+                }
+                if (mForceStop) {
+                    break;
+                }
+                durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+            }
+        }
+    }
+
+    /**
+     * Sleeps until given {@link CountDownLatch} has finished or {@code wakeUpTime} was reached.
+     *
+     * <p>This stops immediately when {@link #cancel()} is called.
+     */
+    private void awaitUntil(CountDownLatch counter, long wakeUpTime) {
+        synchronized (this) {
+            long durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+            while (counter.getCount() > 0 && durationRemaining > 0) {
+                try {
+                    counter.await(durationRemaining, TimeUnit.MILLISECONDS);
+                } catch (InterruptedException e) {
+                }
+                if (mForceStop) {
+                    break;
+                }
+                durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+            }
+        }
+    }
+
+    private void noteVibratorOn(long duration) {
+        try {
+            mBatteryStatsService.noteVibratorOn(mVibration.uid, duration);
+            FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
+                    mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON,
+                    duration);
+        } catch (RemoteException e) {
+        }
+    }
+
+    private void noteVibratorOff() {
+        try {
+            mBatteryStatsService.noteVibratorOff(mVibration.uid);
+            FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
+                    mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF,
+                    /* duration= */ 0);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /** Represent a single synchronized step while playing a {@link CombinedVibrationEffect}. */
+    private interface Step {
+        Vibration.Status play();
+    }
+
+    /** Represent a synchronized vibration step. */
+    private interface VibrateStep extends Step {
+        /** Callback to notify a vibrator has finished playing a effect. */
+        void vibratorComplete(int vibratorId);
+    }
+
+    /** Represent a vibration on a single vibrator. */
+    private final class SingleVibrateStep implements VibrateStep {
+        private final VibratorController mVibrator;
+        private final VibrationEffect mEffect;
+        private final CountDownLatch mCounter;
+
+        SingleVibrateStep(VibratorController vibrator, VibrationEffect effect) {
+            mVibrator = vibrator;
+            mEffect = effect;
+            mCounter = new CountDownLatch(1);
+        }
+
+        @Override
+        public void vibratorComplete(int vibratorId) {
+            if (mVibrator.getVibratorInfo().getId() != vibratorId) {
+                return;
+            }
+            if (mEffect instanceof VibrationEffect.OneShot
+                    || mEffect instanceof VibrationEffect.Waveform) {
+                // Oneshot and Waveform are controlled by amplitude steps, ignore callbacks.
+                return;
+            }
+            mVibrator.off();
+            mCounter.countDown();
+        }
+
+        @Override
+        public Vibration.Status play() {
+            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SingleVibrateStep");
+            long duration = -1;
+            try {
+                if (DEBUG) {
+                    Slog.d(TAG, "SingleVibrateStep starting...");
+                }
+                long startTime = SystemClock.uptimeMillis();
+                duration = vibratePredefined(mEffect);
+
+                if (duration > 0) {
+                    noteVibratorOn(duration);
+                    // Vibration is playing with no need to control amplitudes, just wait for native
+                    // callback or timeout.
+                    awaitUntil(mCounter, startTime + duration + CALLBACKS_EXTRA_TIMEOUT);
+                    return Vibration.Status.FINISHED;
+                }
+
+                startTime = SystemClock.uptimeMillis();
+                AmplitudeStep amplitudeStep = vibrateWithAmplitude(mEffect, startTime);
+                if (amplitudeStep == null) {
+                    // Vibration could not be played with or without amplitude steps.
+                    return Vibration.Status.IGNORED_UNSUPPORTED;
+                }
+
+                duration = mEffect instanceof VibrationEffect.Prebaked
+                        ? ((VibrationEffect.Prebaked) mEffect).getFallbackEffect().getDuration()
+                        : mEffect.getDuration();
+                if (duration < Long.MAX_VALUE) {
+                    // Only report vibration stats if we know how long we will be vibrating.
+                    noteVibratorOn(duration);
+                }
+                while (amplitudeStep != null) {
+                    waitUntil(amplitudeStep.startTime);
+                    if (mForceStop) {
+                        mVibrator.off();
+                        return Vibration.Status.CANCELLED;
+                    }
+                    amplitudeStep.play();
+                    amplitudeStep = amplitudeStep.nextStep();
+                }
+
+                return Vibration.Status.FINISHED;
+            } finally {
+                if (duration > 0 && duration < Long.MAX_VALUE) {
+                    noteVibratorOff();
+                }
+                if (DEBUG) {
+                    Slog.d(TAG, "SingleVibrateStep step done.");
+                }
+                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            }
+        }
+
+        /**
+         * Try to vibrate given effect using prebaked or composed predefined effects.
+         *
+         * @return the duration, in millis, expected for the vibration, or -1 if effect cannot be
+         * played with predefined effects.
+         */
+        private long vibratePredefined(VibrationEffect effect) {
+            if (effect instanceof VibrationEffect.Prebaked) {
+                VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+                long duration = mVibrator.on(prebaked, mVibration.id);
+                if (duration > 0) {
+                    return duration;
+                }
+                if (prebaked.getFallbackEffect() != null) {
+                    return vibratePredefined(prebaked.getFallbackEffect());
+                }
+            } else if (effect instanceof VibrationEffect.Composed) {
+                VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+                return mVibrator.on(composed, mVibration.id);
+            }
+            // OneShot and Waveform effects require amplitude change after calling vibrator.on.
+            return -1;
+        }
+
+        /**
+         * Try to vibrate given effect using {@link AmplitudeStep} to control vibration amplitude.
+         *
+         * @return the {@link AmplitudeStep} to start this vibration, or {@code null} if vibration
+         * do not require amplitude control.
+         */
+        private AmplitudeStep vibrateWithAmplitude(VibrationEffect effect, long startTime) {
+            int vibratorId = mVibrator.getVibratorInfo().getId();
+            if (effect instanceof VibrationEffect.OneShot) {
+                VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect;
+                return new AmplitudeStep(vibratorId, oneShot, startTime, startTime);
+            } else if (effect instanceof VibrationEffect.Waveform) {
+                VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) effect;
+                return new AmplitudeStep(vibratorId, waveform, startTime, startTime);
+            } else if (effect instanceof VibrationEffect.Prebaked) {
+                VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+                if (prebaked.getFallbackEffect() != null) {
+                    return vibrateWithAmplitude(prebaked.getFallbackEffect(), startTime);
+                }
+            }
+            return null;
+        }
+    }
+
+    /** Represent a synchronized vibration step on multiple vibrators. */
+    private final class SyncedVibrateStep implements VibrateStep {
+        private final SparseArray<VibrationEffect> mEffects;
+        private final CountDownLatch mActiveVibratorCounter;
+
+        private final int mRequiredCapabilities;
+        private final int[] mVibratorIds;
+
+        SyncedVibrateStep(SparseArray<VibrationEffect> effects) {
+            mEffects = effects;
+            mActiveVibratorCounter = new CountDownLatch(mEffects.size());
+            // TODO(b/159207608): Calculate required capabilities for syncing this step.
+            mRequiredCapabilities = 0;
+            mVibratorIds = new int[effects.size()];
+            for (int i = 0; i < effects.size(); i++) {
+                mVibratorIds[i] = effects.keyAt(i);
+            }
+        }
+
+        @Override
+        public void vibratorComplete(int vibratorId) {
+            VibrationEffect effect = mEffects.get(vibratorId);
+            if (effect == null) {
+                return;
+            }
+            if (effect instanceof VibrationEffect.OneShot
+                    || effect instanceof VibrationEffect.Waveform) {
+                // Oneshot and Waveform are controlled by amplitude steps, ignore callbacks.
+                return;
+            }
+            mVibrators.get(vibratorId).off();
+            mActiveVibratorCounter.countDown();
+        }
+
+        @Override
+        public Vibration.Status play() {
+            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SyncedVibrateStep");
+            long timeout = -1;
+            try {
+                if (DEBUG) {
+                    Slog.d(TAG, "SyncedVibrateStep starting...");
+                }
+                final PriorityQueue<AmplitudeStep> nextSteps = new PriorityQueue<>(mEffects.size());
+                long startTime = SystemClock.uptimeMillis();
+                mCallbacks.prepareSyncedVibration(mRequiredCapabilities, mVibratorIds);
+                timeout = startVibrating(startTime, nextSteps);
+                mCallbacks.triggerSyncedVibration(mVibration.id);
+                noteVibratorOn(timeout);
+
+                while (!nextSteps.isEmpty()) {
+                    AmplitudeStep step = nextSteps.poll();
+                    waitUntil(step.startTime);
+                    if (mForceStop) {
+                        stopAllVibrators();
+                        return Vibration.Status.CANCELLED;
+                    }
+                    step.play();
+                    AmplitudeStep nextStep = step.nextStep();
+                    if (nextStep == null) {
+                        // This vibrator has finished playing the effect for this step.
+                        mActiveVibratorCounter.countDown();
+                    } else {
+                        nextSteps.add(nextStep);
+                    }
+                }
+
+                // All OneShot and Waveform effects have finished. Just wait for the other effects
+                // to end via native callbacks before finishing this synced step.
+                awaitUntil(mActiveVibratorCounter, startTime + timeout + CALLBACKS_EXTRA_TIMEOUT);
+                return Vibration.Status.FINISHED;
+            } finally {
+                if (timeout > 0) {
+                    noteVibratorOff();
+                }
+                if (DEBUG) {
+                    Slog.d(TAG, "SyncedVibrateStep done.");
+                }
+                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            }
+        }
+
+        /**
+         * Starts playing effects on designated vibrators.
+         *
+         * <p>This includes the {@link VibrationEffect.OneShot} and {@link VibrationEffect.Waveform}
+         * effects, that should start in sync with all other effects in this step. The waveforms are
+         * controlled by {@link AmplitudeStep} added to the {@code nextSteps} queue.
+         *
+         * @return A duration, in millis, to wait for the completion of all vibrations. This ignores
+         * any repeating waveform duration and returns the duration of a single run.
+         */
+        private long startVibrating(long startTime, PriorityQueue<AmplitudeStep> nextSteps) {
+            long maxDuration = 0;
+            for (int i = 0; i < mEffects.size(); i++) {
+                VibratorController controller = mVibrators.get(mEffects.keyAt(i));
+                VibrationEffect effect = mEffects.valueAt(i);
+                maxDuration = Math.max(maxDuration,
+                        startVibrating(controller, effect, startTime, nextSteps));
+            }
+            return maxDuration;
+        }
+
+        /**
+         * Play a single effect on a single vibrator.
+         *
+         * @return A duration, in millis, to wait for the completion of this effect. This ignores
+         * any repeating waveform duration and returns the duration of a single run to be used as
+         * timeout for callbacks.
+         */
+        private long startVibrating(VibratorController controller, VibrationEffect effect,
+                long startTime, PriorityQueue<AmplitudeStep> nextSteps) {
+            int vibratorId = controller.getVibratorInfo().getId();
+            long duration;
+            if (effect instanceof VibrationEffect.OneShot) {
+                VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect;
+                duration = oneShot.getDuration();
+                controller.on(duration, mVibration.id);
+                nextSteps.add(
+                        new AmplitudeStep(vibratorId, oneShot, startTime, startTime + duration));
+            } else if (effect instanceof VibrationEffect.Waveform) {
+                VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) effect;
+                duration = getVibratorOnDuration(waveform, 0);
+                if (duration > 0) {
+                    // Waveform starts by turning vibrator on. Do it in this sync vibrate step.
+                    controller.on(duration, mVibration.id);
+                }
+                nextSteps.add(
+                        new AmplitudeStep(vibratorId, waveform, startTime, startTime + duration));
+            } else if (effect instanceof VibrationEffect.Prebaked) {
+                VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+                duration = controller.on(prebaked, mVibration.id);
+                if (duration <= 0 && prebaked.getFallbackEffect() != null) {
+                    return startVibrating(controller, prebaked.getFallbackEffect(), startTime,
+                            nextSteps);
+                }
+            } else if (effect instanceof VibrationEffect.Composed) {
+                VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+                duration = controller.on(composed, mVibration.id);
+            } else {
+                duration = 0;
+            }
+            return duration;
+        }
+
+        private void stopAllVibrators() {
+            for (int vibratorId : mVibratorIds) {
+                VibratorController controller = mVibrators.get(vibratorId);
+                if (controller != null) {
+                    controller.off();
+                }
+            }
+        }
+    }
+
+    /** Represent a step to set amplitude on a single vibrator. */
+    private final class AmplitudeStep implements Step, Comparable<AmplitudeStep> {
+        public final int vibratorId;
+        public final VibrationEffect.Waveform waveform;
+        public final int currentIndex;
+        public final long startTime;
+        public final long vibratorStopTime;
+
+        AmplitudeStep(int vibratorId, VibrationEffect.OneShot oneShot,
+                long startTime, long vibratorStopTime) {
+            this(vibratorId, (VibrationEffect.Waveform) VibrationEffect.createWaveform(
+                    new long[]{oneShot.getDuration()},
+                    new int[]{oneShot.getAmplitude()}, /* repeat= */ -1),
+                    startTime,
+                    vibratorStopTime);
+        }
+
+        AmplitudeStep(int vibratorId, VibrationEffect.Waveform waveform,
+                long startTime, long vibratorStopTime) {
+            this(vibratorId, waveform, /* index= */ 0, startTime, vibratorStopTime);
+        }
+
+        AmplitudeStep(int vibratorId, VibrationEffect.Waveform waveform,
+                int index, long startTime, long vibratorStopTime) {
+            this.vibratorId = vibratorId;
+            this.waveform = waveform;
+            this.currentIndex = index;
+            this.startTime = startTime;
+            this.vibratorStopTime = vibratorStopTime;
+        }
+
+        @Override
+        public Vibration.Status play() {
+            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "AmplitudeStep");
+            try {
+                if (DEBUG) {
+                    Slog.d(TAG, "AmplitudeStep starting on vibrator " + vibratorId + "...");
+                }
+                VibratorController controller = mVibrators.get(vibratorId);
+                if (currentIndex < 0) {
+                    controller.off();
+                    if (DEBUG) {
+                        Slog.d(TAG, "Vibrator turned off and finishing");
+                    }
+                    return Vibration.Status.FINISHED;
+                }
+                if (waveform.getTimings()[currentIndex] == 0) {
+                    // Skip waveform entries with zero timing.
+                    return Vibration.Status.FINISHED;
+                }
+                int amplitude = waveform.getAmplitudes()[currentIndex];
+                if (amplitude == 0) {
+                    controller.off();
+                    if (DEBUG) {
+                        Slog.d(TAG, "Vibrator turned off");
+                    }
+                    return Vibration.Status.FINISHED;
+                }
+                if (startTime >= vibratorStopTime) {
+                    // Vibrator has stopped. Turn vibrator back on for the duration of another
+                    // cycle before setting the amplitude.
+                    long onDuration = getVibratorOnDuration(waveform, currentIndex);
+                    if (onDuration > 0) {
+                        controller.on(onDuration, mVibration.id);
+                        if (DEBUG) {
+                            Slog.d(TAG, "Vibrator turned on for " + onDuration + "ms");
+                        }
+                    }
+                }
+                controller.setAmplitude(amplitude);
+                if (DEBUG) {
+                    Slog.d(TAG, "Amplitude changed to " + amplitude);
+                }
+                return Vibration.Status.FINISHED;
+            } finally {
+                if (DEBUG) {
+                    Slog.d(TAG, "AmplitudeStep done.");
+                }
+                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            }
+        }
+
+        @Override
+        public int compareTo(AmplitudeStep o) {
+            return Long.compare(startTime, o.startTime);
+        }
+
+        /** Return next {@link AmplitudeStep} from this waveform, of {@code null} if finished. */
+        @Nullable
+        public AmplitudeStep nextStep() {
+            if (currentIndex < 0) {
+                // Waveform has ended, no more steps to run.
+                return null;
+            }
+            long nextWakeUpTime = startTime + waveform.getTimings()[currentIndex];
+            int nextIndex = currentIndex + 1;
+            if (nextIndex >= waveform.getTimings().length) {
+                nextIndex = waveform.getRepeatIndex();
+            }
+            return new AmplitudeStep(vibratorId, waveform, nextIndex, nextWakeUpTime,
+                    nextVibratorStopTime());
+        }
+
+        /** Return next time the vibrator will stop after this step is played. */
+        private long nextVibratorStopTime() {
+            if (currentIndex < 0 || waveform.getTimings()[currentIndex] == 0
+                    || startTime < vibratorStopTime) {
+                return vibratorStopTime;
+            }
+            return startTime + getVibratorOnDuration(waveform, currentIndex);
+        }
+    }
+
+    /** Represent a delay step with fixed duration, that starts counting when it starts playing. */
+    private final class DelayStep implements Step {
+        private final int mDelay;
+
+        DelayStep(int delay) {
+            mDelay = delay;
+        }
+
+        @Override
+        public Vibration.Status play() {
+            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "DelayStep");
+            try {
+                if (DEBUG) {
+                    Slog.d(TAG, "DelayStep of " + mDelay + "ms starting...");
+                }
+                waitUntil(SystemClock.uptimeMillis() + mDelay);
+                return Vibration.Status.FINISHED;
+            } finally {
+                if (DEBUG) {
+                    Slog.d(TAG, "DelayStep done.");
+                }
+                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 311c73b..53f52e2 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -142,6 +142,11 @@
         }
     }
 
+    @VisibleForTesting
+    public NativeWrapper getNativeWrapper() {
+        return mNativeWrapper;
+    }
+
     /** Return the {@link VibratorInfo} representing the vibrator controlled by this instance. */
     public VibratorInfo getVibratorInfo() {
         return mVibratorInfo;
@@ -240,6 +245,8 @@
      * {@link OnVibrationCompleteListener}.
      *
      * <p>This will affect the state of {@link #isVibrating()}.
+     *
+     * @return The duration of the effect playing, or 0 if unsupported.
      */
     public long on(VibrationEffect.Prebaked effect, long vibrationId) {
         synchronized (mLock) {
@@ -257,15 +264,20 @@
      * {@link OnVibrationCompleteListener}.
      *
      * <p>This will affect the state of {@link #isVibrating()}.
+     *
+     * @return The duration of the effect playing, or 0 if unsupported.
      */
-    public void on(VibrationEffect.Composed effect, long vibrationId) {
+    public long on(VibrationEffect.Composed effect, long vibrationId) {
         if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
-            return;
+            return 0;
         }
         synchronized (mLock) {
             mNativeWrapper.compose(effect.getPrimitiveEffects().toArray(
                     new VibrationEffect.Composition.PrimitiveEffect[0]), vibrationId);
             notifyVibratorOnLocked();
+            // Compose don't actually give us an estimated duration, so we just guess here.
+            // TODO(b/177807015): use exposed durations from IVibrator here instead
+            return 20 * effect.getPrimitiveEffects().size();
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
index 726536d..0a35db5 100644
--- a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
@@ -24,10 +24,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -48,6 +44,7 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.server.vibrator.FakeVibratorControllerProvider;
 import com.android.server.vibrator.VibratorController;
 
 import org.junit.After;
@@ -81,7 +78,7 @@
     @Mock private PowerManagerInternal mPowerManagerInternalMock;
     @Mock private PowerSaveState mPowerSaveStateMock;
 
-    private final Map<Integer, VibratorController.NativeWrapper> mNativeWrappers = new HashMap<>();
+    private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
 
     private TestLooper mTestLooper;
 
@@ -117,8 +114,8 @@
                     @Override
                     VibratorController createVibratorController(int vibratorId,
                             VibratorController.OnVibrationCompleteListener listener) {
-                        return new VibratorController(
-                                vibratorId, listener, mNativeWrappers.get(vibratorId));
+                        return mVibratorProviders.get(vibratorId)
+                                .newVibratorController(vibratorId, listener);
                     }
                 });
         service.systemReady();
@@ -126,9 +123,12 @@
     }
 
     @Test
-    public void createService_initializesNativeService() {
+    public void createService_initializesNativeManagerServiceAndVibrators() {
+        mockVibrators(1, 2);
         createService();
         verify(mNativeWrapperMock).init();
+        assertTrue(mVibratorProviders.get(1).isInitialized());
+        assertTrue(mVibratorProviders.get(2).isInitialized());
     }
 
     @Test
@@ -139,28 +139,23 @@
 
     @Test
     public void getVibratorIds_withNonEmptyResultFromNative_returnsSameArray() {
-        mNativeWrappers.put(1, mockVibrator(/* capabilities= */ 0));
-        mNativeWrappers.put(2, mockVibrator(/* capabilities= */ 0));
-        when(mNativeWrapperMock.getVibratorIds()).thenReturn(new int[]{2, 1});
+        mockVibrators(2, 1);
         assertArrayEquals(new int[]{2, 1}, createService().getVibratorIds());
     }
 
     @Test
     public void getVibratorInfo_withMissingVibratorId_returnsNull() {
-        mockVibrators(mockVibrator(/* capabilities= */ 0));
+        mockVibrators(1);
         assertNull(createService().getVibratorInfo(2));
     }
 
     @Test
     public void getVibratorInfo_withExistingVibratorId_returnsHalInfoForVibrator() {
-        VibratorController.NativeWrapper vibratorMock = mockVibrator(
-                IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL);
-        when(vibratorMock.getSupportedEffects()).thenReturn(
-                new int[]{VibrationEffect.EFFECT_CLICK});
-        when(vibratorMock.getSupportedPrimitives()).thenReturn(
-                new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK});
-        mNativeWrappers.put(1, vibratorMock);
-        when(mNativeWrapperMock.getVibratorIds()).thenReturn(new int[]{1});
+        mockVibrators(1);
+        FakeVibratorControllerProvider vibrator = mVibratorProviders.get(1);
+        vibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS, IVibrator.CAP_AMPLITUDE_CONTROL);
+        vibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        vibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK);
         VibratorInfo info = createService().getVibratorInfo(1);
 
         assertNotNull(info);
@@ -178,105 +173,95 @@
 
     @Test
     public void setAlwaysOnEffect_withMono_enablesAlwaysOnEffectToAllVibratorsWithCapability() {
-        VibratorController.NativeWrapper[] vibratorMocks = new VibratorController.NativeWrapper[]{
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
-                mockVibrator(/* capabilities= */ 0),
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
-        };
-        mockVibrators(vibratorMocks);
+        mockVibrators(1, 2, 3);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+        mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
 
         CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
                 VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
         assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
 
-        // Only vibrators 0 and 2 have always-on capabilities.
-        verify(vibratorMocks[0]).alwaysOnEnable(
-                eq(1L), eq((long) VibrationEffect.EFFECT_CLICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
-        verify(vibratorMocks[1], never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
-        verify(vibratorMocks[2]).alwaysOnEnable(
-                eq(1L), eq((long) VibrationEffect.EFFECT_CLICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
+        VibrationEffect.Prebaked expectedEffect = new VibrationEffect.Prebaked(
+                VibrationEffect.EFFECT_CLICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+        // Only vibrators 1 and 3 have always-on capabilities.
+        assertEquals(mVibratorProviders.get(1).getAlwaysOnEffect(1), expectedEffect);
+        assertNull(mVibratorProviders.get(2).getAlwaysOnEffect(1));
+        assertEquals(mVibratorProviders.get(3).getAlwaysOnEffect(1), expectedEffect);
     }
 
     @Test
     public void setAlwaysOnEffect_withStereo_enablesAlwaysOnEffectToAllVibratorsWithCapability() {
-        VibratorController.NativeWrapper[] vibratorMocks = new VibratorController.NativeWrapper[] {
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
-                mockVibrator(0),
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
-        };
-        mockVibrators(vibratorMocks);
+        mockVibrators(1, 2, 3, 4);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+        mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
 
         CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
-                .addVibrator(0, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
-                .addVibrator(1, VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
-                .addVibrator(2, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(1, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(2, VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
+                .addVibrator(3, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
                 .combine();
         assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
 
-        // Enables click on vibrator 0 and tick on vibrator 1 only.
-        verify(vibratorMocks[0]).alwaysOnEnable(
-                eq(1L), eq((long) VibrationEffect.EFFECT_CLICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
-        verify(vibratorMocks[1]).alwaysOnEnable(
-                eq(1L), eq((long) VibrationEffect.EFFECT_TICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
-        verify(vibratorMocks[2], never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
-        verify(vibratorMocks[3], never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
+        VibrationEffect.Prebaked expectedClick = new VibrationEffect.Prebaked(
+                VibrationEffect.EFFECT_CLICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+        VibrationEffect.Prebaked expectedTick = new VibrationEffect.Prebaked(
+                VibrationEffect.EFFECT_TICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+        // Enables click on vibrator 1 and tick on vibrator 2 only.
+        assertEquals(mVibratorProviders.get(1).getAlwaysOnEffect(1), expectedClick);
+        assertEquals(mVibratorProviders.get(2).getAlwaysOnEffect(1), expectedTick);
+        assertNull(mVibratorProviders.get(3).getAlwaysOnEffect(1));
+        assertNull(mVibratorProviders.get(4).getAlwaysOnEffect(1));
     }
 
     @Test
     public void setAlwaysOnEffect_withNullEffect_disablesAlwaysOnEffects() {
-        VibratorController.NativeWrapper[] vibratorMocks = new VibratorController.NativeWrapper[] {
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
-                mockVibrator(0),
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
-        };
-        mockVibrators(vibratorMocks);
+        mockVibrators(1, 2, 3);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+        mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+
+        CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
+                VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+        assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
 
         assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, null, ALARM_ATTRS));
 
-        // Disables only 0 and 2 that have capability.
-        verify(vibratorMocks[0]).alwaysOnDisable(eq(1L));
-        verify(vibratorMocks[1], never()).alwaysOnDisable(anyLong());
-        verify(vibratorMocks[2]).alwaysOnDisable(eq(1L));
+        assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
+        assertNull(mVibratorProviders.get(2).getAlwaysOnEffect(1));
+        assertNull(mVibratorProviders.get(3).getAlwaysOnEffect(1));
     }
 
     @Test
     public void setAlwaysOnEffect_withNonPrebakedEffect_ignoresEffect() {
-        VibratorController.NativeWrapper vibratorMock =
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL);
-        mockVibrators(vibratorMock);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
 
         CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
                 VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
         assertFalse(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
 
-        verify(vibratorMock, never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
-        verify(vibratorMock, never()).alwaysOnDisable(anyLong());
+        assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
     }
 
     @Test
     public void setAlwaysOnEffect_withNonSyncedEffect_ignoresEffect() {
-        VibratorController.NativeWrapper vibratorMock =
-                mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL);
-        mockVibrators(vibratorMock);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
 
         CombinedVibrationEffect effect = CombinedVibrationEffect.startSequential()
                 .addNext(0, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                 .combine();
         assertFalse(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
 
-        verify(vibratorMock, never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
-        verify(vibratorMock, never()).alwaysOnDisable(anyLong());
+        assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
     }
 
     @Test
     public void setAlwaysOnEffect_withNoVibratorWithCapability_ignoresEffect() {
-        VibratorController.NativeWrapper vibratorMock = mockVibrator(0);
-        mockVibrators(vibratorMock);
+        mockVibrators(1);
         VibratorManagerService service = createService();
 
         CombinedVibrationEffect mono = CombinedVibrationEffect.createSynced(
@@ -287,8 +272,7 @@
         assertFalse(service.setAlwaysOnEffect(UID, PACKAGE_NAME, 1, mono, ALARM_ATTRS));
         assertFalse(service.setAlwaysOnEffect(UID, PACKAGE_NAME, 2, stereo, ALARM_ATTRS));
 
-        verify(vibratorMock, never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
-        verify(vibratorMock, never()).alwaysOnDisable(anyLong());
+        assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
     }
 
     @Test
@@ -310,19 +294,12 @@
                 "Not implemented", () -> service.cancelVibrate(service));
     }
 
-    private VibratorController.NativeWrapper mockVibrator(int capabilities) {
-        VibratorController.NativeWrapper wrapper = mock(VibratorController.NativeWrapper.class);
-        when(wrapper.getCapabilities()).thenReturn((long) capabilities);
-        return wrapper;
-    }
-
-    private void mockVibrators(VibratorController.NativeWrapper... wrappers) {
-        int[] ids = new int[wrappers.length];
-        for (int i = 0; i < wrappers.length; i++) {
-            ids[i] = i;
-            mNativeWrappers.put(i, wrappers[i]);
+    private void mockVibrators(int... vibratorIds) {
+        for (int vibratorId : vibratorIds) {
+            mVibratorProviders.put(vibratorId,
+                    new FakeVibratorControllerProvider(mTestLooper.getLooper()));
         }
-        when(mNativeWrapperMock.getVibratorIds()).thenReturn(ids);
+        when(mNativeWrapperMock.getVibratorIds()).thenReturn(vibratorIds);
     }
 
     private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
diff --git a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
index 32ca7b5..92256e2 100644
--- a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
@@ -19,21 +19,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.AdditionalMatchers.gt;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.intThat;
-import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
@@ -54,8 +50,6 @@
 import android.os.PowerManagerInternal;
 import android.os.PowerSaveState;
 import android.os.Process;
-import android.os.RemoteException;
-import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
@@ -70,16 +64,15 @@
 
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.internal.util.test.FakeSettingsProviderRule;
+import com.android.server.vibrator.FakeVibratorControllerProvider;
 import com.android.server.vibrator.VibratorController;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -87,7 +80,7 @@
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
 
 /**
  * Tests for {@link VibratorService}.
@@ -99,6 +92,7 @@
 public class VibratorServiceTest {
 
     private static final int UID = Process.ROOT_UID;
+    private static final int VIBRATOR_ID = 1;
     private static final String PACKAGE_NAME = "package";
     private static final PowerSaveState NORMAL_POWER_STATE = new PowerSaveState.Builder().build();
     private static final PowerSaveState LOW_POWER_STATE = new PowerSaveState.Builder()
@@ -123,7 +117,6 @@
     // TODO(b/131311651): replace with a FakeVibrator instead.
     @Mock private Vibrator mVibratorMock;
     @Mock private AppOpsManager mAppOpsManagerMock;
-    @Mock private VibratorController.NativeWrapper mNativeWrapperMock;
     @Mock private IVibratorStateListener mVibratorStateListenerMock;
     @Mock private IInputManager mIInputManagerMock;
     @Mock private IBinder mVibratorStateListenerBinderMock;
@@ -131,10 +124,12 @@
     private TestLooper mTestLooper;
     private ContextWrapper mContextSpy;
     private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
+    private FakeVibratorControllerProvider mVibratorProvider;
 
     @Before
     public void setUp() throws Exception {
         mTestLooper = new TestLooper();
+        mVibratorProvider = new FakeVibratorControllerProvider(mTestLooper.getLooper());
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
         InputManager inputManager = InputManager.resetInstance(mIInputManagerMock);
 
@@ -183,7 +178,7 @@
                     @Override
                     VibratorController createVibratorController(
                             VibratorController.OnVibrationCompleteListener listener) {
-                        return new VibratorController(0, listener, mNativeWrapperMock);
+                        return mVibratorProvider.newVibratorController(VIBRATOR_ID, listener);
                     }
 
                     @Override
@@ -203,25 +198,23 @@
     @Test
     public void createService_initializesNativeService() {
         createService();
-        verify(mNativeWrapperMock).init(eq(0), notNull());
-        verify(mNativeWrapperMock, times(2)).off(); // Called from constructor and onSystemReady
+        assertTrue(mVibratorProvider.isInitialized());
     }
 
     @Test
     public void hasVibrator_withVibratorHalPresent_returnsTrue() {
-        when(mNativeWrapperMock.isAvailable()).thenReturn(true);
         assertTrue(createService().hasVibrator());
     }
 
     @Test
     public void hasVibrator_withNoVibratorHalPresent_returnsFalse() {
-        when(mNativeWrapperMock.isAvailable()).thenReturn(false);
+        mVibratorProvider.disableVibrators();
         assertFalse(createService().hasVibrator());
     }
 
     @Test
     public void hasAmplitudeControl_withAmplitudeControlSupport_returnsTrue() {
-        mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
         assertTrue(createService().hasAmplitudeControl());
     }
 
@@ -234,18 +227,17 @@
     public void hasAmplitudeControl_withInputDevices_returnsTrue() throws Exception {
         when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1});
         when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
-        mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
         setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
         assertTrue(createService().hasAmplitudeControl());
     }
 
     @Test
     public void getVibratorInfo_returnsSameInfoFromNative() {
-        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL);
-        when(mNativeWrapperMock.getSupportedEffects())
-                .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
-        when(mNativeWrapperMock.getSupportedPrimitives())
-                .thenReturn(new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK});
+        mVibratorProvider.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS,
+                IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProvider.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        mVibratorProvider.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK);
 
         VibratorInfo info = createService().getVibratorInfo();
         assertTrue(info.hasAmplitudeControl());
@@ -258,7 +250,7 @@
     }
 
     @Test
-    public void vibrate_withRingtone_usesRingtoneSettings() {
+    public void vibrate_withRingtone_usesRingtoneSettings() throws Exception {
         setRingerMode(AudioManager.RINGER_MODE_NORMAL);
         setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
         setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
@@ -266,34 +258,34 @@
 
         setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
         setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 1);
-        vibrate(createService(), VibrationEffect.createOneShot(10, 10), RINGTONE_ATTRS);
+        vibrateAndWait(createService(), VibrationEffect.createOneShot(10, 10), RINGTONE_ATTRS);
 
         setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1);
         setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
-        vibrate(createService(), VibrationEffect.createOneShot(100, 100), RINGTONE_ATTRS);
+        vibrateAndWait(createService(), VibrationEffect.createOneShot(100, 100), RINGTONE_ATTRS);
 
-        InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
-        inOrderVerifier.verify(mNativeWrapperMock, never()).on(eq(1L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(10L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(100L), anyLong());
+        List<VibrationEffect> effects = mVibratorProvider.getEffects();
+        assertEquals(2, effects.size());
+        assertEquals(10, effects.get(0).getDuration());
+        assertEquals(100, effects.get(1).getDuration());
     }
 
     @Test
-    public void vibrate_withPowerModeChange_usesLowPowerModeState() {
+    public void vibrate_withPowerModeChange_usesLowPowerModeState() throws Exception {
         VibratorService service = createService();
         mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
         vibrate(service, VibrationEffect.createOneShot(1, 1), HAPTIC_FEEDBACK_ATTRS);
-        vibrate(service, VibrationEffect.createOneShot(2, 2), RINGTONE_ATTRS);
+        vibrateAndWait(service, VibrationEffect.createOneShot(2, 2), RINGTONE_ATTRS);
 
         mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
-        vibrate(service, VibrationEffect.createOneShot(3, 3), /* attributes= */ null);
-        vibrate(service, VibrationEffect.createOneShot(4, 4), NOTIFICATION_ATTRS);
+        vibrateAndWait(service, VibrationEffect.createOneShot(3, 3), /* attributes= */ null);
+        vibrateAndWait(service, VibrationEffect.createOneShot(4, 4), NOTIFICATION_ATTRS);
 
-        InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
-        inOrderVerifier.verify(mNativeWrapperMock, never()).on(eq(1L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(2L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(3L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(4L), anyLong());
+        List<VibrationEffect> effects = mVibratorProvider.getEffects();
+        assertEquals(3, effects.size());
+        assertEquals(2, effects.get(0).getDuration());
+        assertEquals(3, effects.get(1).getDuration());
+        assertEquals(4, effects.get(2).getDuration());
     }
 
     @Test
@@ -349,55 +341,55 @@
         when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
         setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
         VibrationEffect effect = VibrationEffect.createOneShot(100, 128);
-        vibrate(service, effect);
-        assertFalse(service.isVibrating());
-
+        vibrate(service, effect, ALARM_ATTRS);
         verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
-        verify(mNativeWrapperMock, never()).on(anyLong(), anyLong());
+
+        // VibrationThread will start this vibration async, so wait before checking it never played.
+        Thread.sleep(10);
+        assertTrue(mVibratorProvider.getEffects().isEmpty());
     }
 
     @Test
-    public void vibrate_withOneShotAndAmplitudeControl_turnsVibratorOnAndSetsAmplitude() {
-        mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+    public void vibrate_withOneShotAndAmplitudeControl_turnsVibratorOnAndSetsAmplitude()
+            throws Exception {
+        mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
-        vibrate(service, VibrationEffect.createOneShot(100, 128));
-        assertTrue(service.isVibrating());
+        vibrateAndWait(service, VibrationEffect.createOneShot(100, 128), ALARM_ATTRS);
 
-        verify(mNativeWrapperMock).off();
-        verify(mNativeWrapperMock).on(eq(100L), gt(0L));
-        verify(mNativeWrapperMock).setAmplitude(eq(128));
+        List<VibrationEffect> effects = mVibratorProvider.getEffects();
+        assertEquals(1, effects.size());
+        assertEquals(100, effects.get(0).getDuration());
+        assertEquals(Arrays.asList(128), mVibratorProvider.getAmplitudes());
     }
 
     @Test
-    public void vibrate_withOneShotAndNoAmplitudeControl_turnsVibratorOnAndIgnoresAmplitude() {
+    public void vibrate_withOneShotAndNoAmplitudeControl_turnsVibratorOnAndIgnoresAmplitude()
+            throws Exception {
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
+        clearInvocations();
 
-        vibrate(service, VibrationEffect.createOneShot(100, 128));
-        assertTrue(service.isVibrating());
+        vibrateAndWait(service, VibrationEffect.createOneShot(100, 128), ALARM_ATTRS);
 
-        verify(mNativeWrapperMock).off();
-        verify(mNativeWrapperMock).on(eq(100L), gt(0L));
-        verify(mNativeWrapperMock, never()).setAmplitude(anyInt());
+        List<VibrationEffect> effects = mVibratorProvider.getEffects();
+        assertEquals(1, effects.size());
+        assertEquals(100, effects.get(0).getDuration());
+        assertTrue(mVibratorProvider.getAmplitudes().isEmpty());
     }
 
     @Test
-    public void vibrate_withPrebaked_performsEffect() {
-        when(mNativeWrapperMock.getSupportedEffects())
-                .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
+    public void vibrate_withPrebaked_performsEffect() throws Exception {
+        mVibratorProvider.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
-        vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+        VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+        vibrateAndWait(service, effect, ALARM_ATTRS);
 
-        verify(mNativeWrapperMock).off();
-        verify(mNativeWrapperMock).perform(eq((long) VibrationEffect.EFFECT_CLICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG), gt(0L));
+        VibrationEffect.Prebaked expectedEffect = new VibrationEffect.Prebaked(
+                VibrationEffect.EFFECT_CLICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+        assertEquals(Arrays.asList(expectedEffect), mVibratorProvider.getEffects());
     }
 
     @Test
@@ -407,120 +399,62 @@
         when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
         setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
-        assertFalse(service.isVibrating());
-
-        // Wait for VibrateThread to turn input device vibrator ON.
-        Thread.sleep(5);
+        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS);
         verify(mIInputManagerMock).vibrate(eq(1), any(), any());
-        verify(mNativeWrapperMock, never()).on(anyLong(), anyLong());
-        verify(mNativeWrapperMock, never()).perform(anyLong(), anyLong(), anyLong());
+
+        // VibrationThread will start this vibration async, so wait before checking it never played.
+        Thread.sleep(10);
+        assertTrue(mVibratorProvider.getEffects().isEmpty());
     }
 
     @Test
-    public void vibrate_withComposed_performsEffect() {
-        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+    public void vibrate_withComposed_performsEffect() throws Exception {
+        mVibratorProvider.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
                 .compose();
-        vibrate(service, effect);
-
-        ArgumentCaptor<VibrationEffect.Composition.PrimitiveEffect[]> primitivesCaptor =
-                ArgumentCaptor.forClass(VibrationEffect.Composition.PrimitiveEffect[].class);
-
-        verify(mNativeWrapperMock).off();
-        verify(mNativeWrapperMock).compose(primitivesCaptor.capture(), gt(0L));
-
-        // Check all primitive effect fields are passed down to the HAL.
-        assertEquals(1, primitivesCaptor.getValue().length);
-        VibrationEffect.Composition.PrimitiveEffect primitive = primitivesCaptor.getValue()[0];
-        assertEquals(VibrationEffect.Composition.PRIMITIVE_CLICK, primitive.id);
-        assertEquals(0.5f, primitive.scale, /* delta= */ 1e-2);
-        assertEquals(10, primitive.delay);
+        vibrateAndWait(service, effect, ALARM_ATTRS);
+        assertEquals(Arrays.asList(effect), mVibratorProvider.getEffects());
     }
 
     @Test
-    public void vibrate_withComposedAndInputDevices_vibratesInputDevices()
-            throws Exception {
+    public void vibrate_withComposedAndInputDevices_vibratesInputDevices() throws Exception {
         when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1, 2});
         when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
         when(mIInputManagerMock.getInputDevice(2)).thenReturn(createInputDeviceWithVibrator(2));
         setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
                 .compose();
-        vibrate(service, effect);
-        assertFalse(service.isVibrating());
+        vibrate(service, effect, ALARM_ATTRS);
+        InOrder inOrderVerifier = inOrder(mIInputManagerMock);
+        inOrderVerifier.verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
+        inOrderVerifier.verify(mIInputManagerMock).vibrate(eq(2), eq(effect), any());
 
-        verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
-        verify(mIInputManagerMock).vibrate(eq(2), eq(effect), any());
-        verify(mNativeWrapperMock, never()).compose(any(), anyLong());
+        // VibrationThread will start this vibration async, so wait before checking it never played.
+        Thread.sleep(10);
+        assertTrue(mVibratorProvider.getEffects().isEmpty());
     }
 
     @Test
     public void vibrate_withWaveform_controlsVibratorAmplitudeDuringTotalVibrationTime()
             throws Exception {
-        mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{10, 10, 10}, new int[]{100, 200, 50}, -1);
-        vibrate(service, effect);
+        vibrateAndWait(service, effect, ALARM_ATTRS);
 
-        // Wait for VibrateThread to finish: 10ms 100, 10ms 200, 10ms 50.
-        Thread.sleep(40);
-        InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(30L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).setAmplitude(eq(100));
-        inOrderVerifier.verify(mNativeWrapperMock).setAmplitude(eq(200));
-        inOrderVerifier.verify(mNativeWrapperMock).setAmplitude(eq(50));
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-    }
-
-    @Test
-    public void vibrate_withWaveform_totalVibrationTimeRespected() throws Exception {
-        int totalDuration = 10_000; // 10s
-        int stepDuration = 25; // 25ms
-
-        // 25% of the first waveform step will be spent on the native on() call.
-        mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
-        doAnswer(invocation -> {
-            Thread.currentThread().sleep(stepDuration / 4);
-            return null;
-        }).when(mNativeWrapperMock).on(anyLong(), anyLong());
-        // 25% of each waveform step will be spent on the native setAmplitude() call..
-        doAnswer(invocation -> {
-            Thread.currentThread().sleep(stepDuration / 4);
-            return null;
-        }).when(mNativeWrapperMock).setAmplitude(anyInt());
-
-        VibratorService service = createService();
-
-        int stepCount = totalDuration / stepDuration;
-        long[] timings = new long[stepCount];
-        int[] amplitudes = new int[stepCount];
-        Arrays.fill(timings, stepDuration);
-        Arrays.fill(amplitudes, VibrationEffect.DEFAULT_AMPLITUDE);
-        VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, -1);
-
-        int perceivedDuration = vibrateAndMeasure(service, effect, /* timeoutSecs= */ 15);
-        int delay = Math.abs(perceivedDuration - totalDuration);
-
-        // Allow some delay for thread scheduling and callback triggering.
-        int maxDelay = (int) (0.05 * totalDuration); // < 5% of total duration
-        assertTrue("Waveform with perceived delay of " + delay + "ms,"
-                        + " expected less than " + maxDelay + "ms",
-                delay < maxDelay);
+        assertEquals(Arrays.asList(100, 200, 50), mVibratorProvider.getAmplitudes());
+        assertEquals(
+                Arrays.asList(VibrationEffect.createOneShot(30, VibrationEffect.DEFAULT_AMPLITUDE)),
+                mVibratorProvider.getEffects());
     }
 
     @Test
@@ -529,123 +463,52 @@
         when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
         setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{10, 10, 10}, new int[]{100, 200, 50}, -1);
-        vibrate(service, effect);
-        assertFalse(service.isVibrating());
-
-        // Wait for VibrateThread to turn input device vibrator ON.
-        Thread.sleep(5);
+        vibrate(service, effect, ALARM_ATTRS);
         verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
-        verify(mNativeWrapperMock, never()).on(anyLong(), anyLong());
+
+        // VibrationThread will start this vibration async, so wait before checking it never played.
+        Thread.sleep(10);
+        assertTrue(mVibratorProvider.getEffects().isEmpty());
     }
 
     @Test
-    public void vibrate_withOneShotAndNativeCallbackTriggered_finishesVibration() {
+    public void vibrate_withNativeCallbackTriggered_finishesVibration() throws Exception {
+        mVibratorProvider.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
         VibratorService service = createService();
-        doAnswer(invocation -> {
-            service.onVibrationComplete(invocation.getArgument(1));
-            return null;
-        }).when(mNativeWrapperMock).on(anyLong(), anyLong());
-        Mockito.clearInvocations(mNativeWrapperMock);
 
-        vibrate(service, VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
+        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS);
 
-        InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(100L), gt(0L));
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-    }
-
-    @Test
-    public void vibrate_withPrebakedAndNativeCallbackTriggered_finishesVibration() {
-        when(mNativeWrapperMock.getSupportedEffects())
-                .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
-        VibratorService service = createService();
-        doAnswer(invocation -> {
-            service.onVibrationComplete(invocation.getArgument(2));
-            return 10_000L; // 10s
-        }).when(mNativeWrapperMock).perform(anyLong(), anyLong(), anyLong());
-        Mockito.clearInvocations(mNativeWrapperMock);
-
-        vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
-
-        InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-        inOrderVerifier.verify(mNativeWrapperMock).perform(
-                eq((long) VibrationEffect.EFFECT_CLICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG),
-                gt(0L));
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-    }
-
-    @Test
-    public void vibrate_withWaveformAndNativeCallback_callbackIgnoredAndWaveformPlaysCompletely()
-            throws Exception {
-        VibratorService service = createService();
-        doAnswer(invocation -> {
-            service.onVibrationComplete(invocation.getArgument(1));
-            return null;
-        }).when(mNativeWrapperMock).on(anyLong(), anyLong());
-        Mockito.clearInvocations(mNativeWrapperMock);
-
-        VibrationEffect effect = VibrationEffect.createWaveform(new long[]{1, 3, 1, 2}, -1);
-        vibrate(service, effect);
-
-        // Wait for VibrateThread to finish: 1ms OFF, 3ms ON, 1ms OFF, 2ms ON.
-        Thread.sleep(15);
-        InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
-        inOrderVerifier.verify(mNativeWrapperMock, times(2)).off();
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(3L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-        inOrderVerifier.verify(mNativeWrapperMock).on(eq(2L), anyLong());
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-    }
-
-    @Test
-    public void vibrate_withComposedAndNativeCallbackTriggered_finishesVibration() {
-        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
-        VibratorService service = createService();
-        doAnswer(invocation -> {
-            service.onVibrationComplete(invocation.getArgument(1));
-            return null;
-        }).when(mNativeWrapperMock).compose(any(), anyLong());
-        Mockito.clearInvocations(mNativeWrapperMock);
-
-        VibrationEffect effect = VibrationEffect.startComposition()
-                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 10)
-                .compose();
-        vibrate(service, effect);
-
-        InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-        inOrderVerifier.verify(mNativeWrapperMock).compose(
-                any(VibrationEffect.Composition.PrimitiveEffect[].class), gt(0L));
-        inOrderVerifier.verify(mNativeWrapperMock).off();
-    }
-
-    @Test
-    public void cancelVibrate_withDeviceVibrating_callsoff() {
-        VibratorService service = createService();
-        vibrate(service, VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
+        // VibrationThread will start this vibration async, so wait before triggering callbacks.
+        Thread.sleep(10);
         assertTrue(service.isVibrating());
-        Mockito.clearInvocations(mNativeWrapperMock);
 
-        service.cancelVibrate(service);
+        // Trigger callbacks from controller.
+        mTestLooper.moveTimeForward(50);
+        mTestLooper.dispatchAll();
+
+        // VibrationThread needs some time to react to native callbacks and stop the vibrator.
+        Thread.sleep(10);
         assertFalse(service.isVibrating());
-        verify(mNativeWrapperMock).off();
     }
 
     @Test
-    public void cancelVibrate_withDeviceNotVibrating_ignoresCall() {
+    public void cancelVibrate_withDeviceVibrating_callsOff() throws Exception {
         VibratorService service = createService();
-        Mockito.clearInvocations(mNativeWrapperMock);
+
+        vibrate(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait before checking.
+        Thread.sleep(10);
+        assertTrue(service.isVibrating());
 
         service.cancelVibrate(service);
+
+        // VibrationThread will stop this vibration async, so wait before checking.
+        Thread.sleep(10);
         assertFalse(service.isVibrating());
-        verify(mNativeWrapperMock, never()).off();
     }
 
     @Test
@@ -653,12 +516,11 @@
         VibratorService service = createService();
         service.registerVibratorStateListener(mVibratorStateListenerMock);
 
-        vibrate(service, VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE));
-        service.cancelVibrate(service);
+        vibrateAndWait(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS);
 
         InOrder inOrderVerifier = inOrder(mVibratorStateListenerMock);
         // First notification done when listener is registered.
-        inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(false);
+        inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(false));
         inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(true));
         inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(false));
         inOrderVerifier.verifyNoMoreInteractions();
@@ -671,18 +533,25 @@
         service.registerVibratorStateListener(mVibratorStateListenerMock);
         verify(mVibratorStateListenerMock).onVibrating(false);
 
-        vibrate(service, VibrationEffect.createOneShot(5, VibrationEffect.DEFAULT_AMPLITUDE));
-        verify(mVibratorStateListenerMock).onVibrating(true);
+        vibrate(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS);
 
+        // VibrationThread will start this vibration async, so wait before triggering callbacks.
+        Thread.sleep(10);
         service.unregisterVibratorStateListener(mVibratorStateListenerMock);
-        Mockito.clearInvocations(mVibratorStateListenerMock);
+        // Trigger callbacks from controller.
+        mTestLooper.moveTimeForward(150);
+        mTestLooper.dispatchAll();
 
-        vibrate(service, VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE));
-        verifyNoMoreInteractions(mVibratorStateListenerMock);
+        InOrder inOrderVerifier = inOrder(mVibratorStateListenerMock);
+        // First notification done when listener is registered.
+        inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(false));
+        inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(true));
+        inOrderVerifier.verify(mVibratorStateListenerMock, atLeastOnce()).asBinder(); // unregister
+        inOrderVerifier.verifyNoMoreInteractions();
     }
 
     @Test
-    public void scale_withPrebaked_userIntensitySettingAsEffectStrength() {
+    public void scale_withPrebaked_userIntensitySettingAsEffectStrength() throws Exception {
         // Alarm vibration is always VIBRATION_INTENSITY_HIGH.
         setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
                 Vibrator.VIBRATION_INTENSITY_MEDIUM);
@@ -690,28 +559,29 @@
                 Vibrator.VIBRATION_INTENSITY_LOW);
         setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
                 Vibrator.VIBRATION_INTENSITY_OFF);
+        mVibratorProvider.setSupportedEffects(
+                VibrationEffect.EFFECT_CLICK,
+                VibrationEffect.EFFECT_TICK,
+                VibrationEffect.EFFECT_DOUBLE_CLICK,
+                VibrationEffect.EFFECT_HEAVY_CLICK);
         VibratorService service = createService();
 
-        vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
-                ALARM_ATTRS);
-        vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK),
+        vibrateAndWait(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS);
+        vibrateAndWait(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK),
                 NOTIFICATION_ATTRS);
-        vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_DOUBLE_CLICK),
+        vibrateAndWait(service, VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK),
                 HAPTIC_FEEDBACK_ATTRS);
-        vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK),
-                RINGTONE_ATTRS);
+        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK), RINGTONE_ATTRS);
 
-        verify(mNativeWrapperMock).perform(
-                eq((long) VibrationEffect.EFFECT_CLICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG), anyLong());
-        verify(mNativeWrapperMock).perform(
-                eq((long) VibrationEffect.EFFECT_TICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_MEDIUM), anyLong());
-        verify(mNativeWrapperMock).perform(
-                eq((long) VibrationEffect.EFFECT_DOUBLE_CLICK),
-                eq((long) VibrationEffect.EFFECT_STRENGTH_LIGHT), anyLong());
-        verify(mNativeWrapperMock, never()).perform(
-                eq((long) VibrationEffect.EFFECT_HEAVY_CLICK), anyLong(), anyLong());
+        List<Integer> playedStrengths = mVibratorProvider.getEffects().stream()
+                .map(VibrationEffect.Prebaked.class::cast)
+                .map(VibrationEffect.Prebaked::getEffectStrength)
+                .collect(Collectors.toList());
+        assertEquals(Arrays.asList(
+                VibrationEffect.EFFECT_STRENGTH_STRONG,
+                VibrationEffect.EFFECT_STRENGTH_MEDIUM,
+                VibrationEffect.EFFECT_STRENGTH_LIGHT),
+                playedStrengths);
     }
 
     @Test
@@ -725,31 +595,28 @@
         setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
                 Vibrator.VIBRATION_INTENSITY_OFF);
 
-        mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
         VibratorService service = createService();
 
-        vibrate(service, VibrationEffect.createOneShot(20, 100), ALARM_ATTRS);
-        vibrate(service, VibrationEffect.createOneShot(20, 100), NOTIFICATION_ATTRS);
-        vibrate(service, VibrationEffect.createOneShot(20, 255), RINGTONE_ATTRS);
-        vibrate(service, VibrationEffect.createWaveform(new long[] { 10 }, new int[] { 100 }, -1),
+        vibrateAndWait(service, VibrationEffect.createOneShot(20, 100), ALARM_ATTRS);
+        vibrateAndWait(service, VibrationEffect.createOneShot(20, 100), NOTIFICATION_ATTRS);
+        vibrateAndWait(service,
+                VibrationEffect.createWaveform(new long[]{10}, new int[]{100}, -1),
                 HAPTIC_FEEDBACK_ATTRS);
+        vibrate(service, VibrationEffect.createOneShot(20, 255), RINGTONE_ATTRS);
 
-        // Waveform effect runs on a separate thread.
-        Thread.sleep(15);
-
+        List<Integer> amplitudes = mVibratorProvider.getAmplitudes();
+        assertEquals(3, amplitudes.size());
         // Alarm vibration is never scaled.
-        verify(mNativeWrapperMock).setAmplitude(eq(100));
+        assertEquals(100, amplitudes.get(0).intValue());
         // Notification vibrations will be scaled with SCALE_VERY_HIGH.
-        verify(mNativeWrapperMock).setAmplitude(intThat(amplitude -> amplitude > 150));
+        assertTrue(amplitudes.get(1) > 150);
         // Haptic feedback vibrations will be scaled with SCALE_LOW.
-        verify(mNativeWrapperMock).setAmplitude(
-                intThat(amplitude -> amplitude < 100 && amplitude > 50));
-        // Ringtone vibration is off.
-        verify(mNativeWrapperMock, never()).setAmplitude(eq(255));
+        assertTrue(amplitudes.get(2) < 100 && amplitudes.get(2) > 50);
     }
 
     @Test
-    public void scale_withComposed_usesScaleLevelOnPrimitiveScaleValues() {
+    public void scale_withComposed_usesScaleLevelOnPrimitiveScaleValues() throws Exception {
         when(mVibratorMock.getDefaultNotificationVibrationIntensity())
                 .thenReturn(Vibrator.VIBRATION_INTENSITY_LOW);
         setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
@@ -759,82 +626,72 @@
         setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
                 Vibrator.VIBRATION_INTENSITY_OFF);
 
-        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        mVibratorProvider.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
         VibratorService service = createService();
 
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .compose();
-        ArgumentCaptor<VibrationEffect.Composition.PrimitiveEffect[]> primitivesCaptor =
-                ArgumentCaptor.forClass(VibrationEffect.Composition.PrimitiveEffect[].class);
 
-        vibrate(service, effect, ALARM_ATTRS);
-        vibrate(service, effect, NOTIFICATION_ATTRS);
-        vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+        vibrateAndWait(service, effect, ALARM_ATTRS);
+        vibrateAndWait(service, effect, NOTIFICATION_ATTRS);
+        vibrateAndWait(service, effect, HAPTIC_FEEDBACK_ATTRS);
         vibrate(service, effect, RINGTONE_ATTRS);
 
-        // Ringtone vibration is off, so only the other 3 are propagated to native.
-        verify(mNativeWrapperMock, times(3)).compose(
-                primitivesCaptor.capture(), anyLong());
+        List<VibrationEffect.Composition.PrimitiveEffect> primitives =
+                mVibratorProvider.getEffects().stream()
+                        .map(VibrationEffect.Composed.class::cast)
+                        .map(VibrationEffect.Composed::getPrimitiveEffects)
+                        .flatMap(List::stream)
+                        .collect(Collectors.toList());
 
-        List<VibrationEffect.Composition.PrimitiveEffect[]> values =
-                primitivesCaptor.getAllValues();
+        // Ringtone vibration is off, so only the other 3 are propagated to native.
+        assertEquals(6, primitives.size());
 
         // Alarm vibration is never scaled.
-        assertEquals(1f, values.get(0)[0].scale, /* delta= */ 1e-2);
-        assertEquals(0.5f, values.get(0)[1].scale, /* delta= */ 1e-2);
+        assertEquals(1f, primitives.get(0).scale, /* delta= */ 1e-2);
+        assertEquals(0.5f, primitives.get(1).scale, /* delta= */ 1e-2);
 
         // Notification vibrations will be scaled with SCALE_VERY_HIGH.
-        assertEquals(1f, values.get(1)[0].scale, /* delta= */ 1e-2);
-        assertTrue(0.7 < values.get(1)[1].scale);
+        assertEquals(1f, primitives.get(2).scale, /* delta= */ 1e-2);
+        assertTrue(0.7 < primitives.get(3).scale);
 
         // Haptic feedback vibrations will be scaled with SCALE_LOW.
-        assertTrue(0.5 < values.get(2)[0].scale);
-        assertTrue(0.5 > values.get(2)[1].scale);
-    }
-
-    private void vibrate(VibratorService service, VibrationEffect effect) {
-        vibrate(service, effect, ALARM_ATTRS);
+        assertTrue(0.5 < primitives.get(4).scale);
+        assertTrue(0.5 > primitives.get(5).scale);
     }
 
     private void vibrate(VibratorService service, VibrationEffect effect,
-            VibrationAttributes attributes) {
-        service.vibrate(UID, PACKAGE_NAME, effect, attributes, "some reason", service);
+            VibrationAttributes attrs) {
+        service.vibrate(UID, PACKAGE_NAME, effect, attrs, "some reason", service);
     }
 
-    private int vibrateAndMeasure(
-            VibratorService service, VibrationEffect effect, long timeoutSecs) throws Exception {
-        AtomicLong startTime = new AtomicLong(0);
-        AtomicLong endTime = new AtomicLong(0);
+    private void vibrateAndWait(VibratorService service, VibrationEffect effect,
+            VibrationAttributes attrs) throws Exception {
         CountDownLatch startedCount = new CountDownLatch(1);
         CountDownLatch finishedCount = new CountDownLatch(1);
         service.registerVibratorStateListener(new IVibratorStateListener() {
             @Override
-            public void onVibrating(boolean vibrating) throws RemoteException {
+            public void onVibrating(boolean vibrating) {
                 if (vibrating) {
-                    startTime.set(SystemClock.uptimeMillis());
                     startedCount.countDown();
                 } else if (startedCount.getCount() == 0) {
-                    endTime.set(SystemClock.uptimeMillis());
                     finishedCount.countDown();
                 }
             }
 
             @Override
             public IBinder asBinder() {
-                return mVibratorStateListenerBinderMock;
+                return mock(IBinder.class);
             }
         });
 
-        vibrate(service, effect);
-
-        assertTrue(finishedCount.await(timeoutSecs, TimeUnit.SECONDS));
-        return (int) (endTime.get() - startTime.get());
-    }
-
-    private void mockVibratorCapabilities(int capabilities) {
-        when(mNativeWrapperMock.getCapabilities()).thenReturn((long) capabilities);
+        mTestLooper.startAutoDispatch();
+        service.vibrate(UID, PACKAGE_NAME, effect, attrs, "some reason", service);
+        assertTrue(startedCount.await(1, TimeUnit.SECONDS));
+        assertTrue(finishedCount.await(1, TimeUnit.SECONDS));
+        mTestLooper.stopAutoDispatchAndIgnoreExceptions();
     }
 
     private InputDevice createInputDeviceWithVibrator(int id) {
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
new file mode 100644
index 0000000..f562c16
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2020 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.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.VibrationEffect;
+
+import com.android.server.vibrator.VibratorController.OnVibrationCompleteListener;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides {@link VibratorController} with controlled vibrator hardware capabilities and
+ * interactions.
+ */
+public final class FakeVibratorControllerProvider {
+
+    private static final int EFFECT_DURATION = 20;
+
+    private final Map<Long, VibrationEffect.Prebaked> mEnabledAlwaysOnEffects = new HashMap<>();
+    private final List<VibrationEffect> mEffects = new ArrayList<>();
+    private final List<Integer> mAmplitudes = new ArrayList<>();
+    private final Handler mHandler;
+    private final FakeNativeWrapper mNativeWrapper;
+
+    private boolean mIsAvailable = true;
+    private long mLatency;
+
+    private int mCapabilities;
+    private int[] mSupportedEffects;
+    private int[] mSupportedPrimitives;
+
+    private final class FakeNativeWrapper extends VibratorController.NativeWrapper {
+        public int vibratorId;
+        public OnVibrationCompleteListener listener;
+        public boolean isInitialized;
+
+        public void init(int vibratorId, OnVibrationCompleteListener listener) {
+            isInitialized = true;
+            this.vibratorId = vibratorId;
+            this.listener = listener;
+        }
+
+        public boolean isAvailable() {
+            return mIsAvailable;
+        }
+
+        public void on(long milliseconds, long vibrationId) {
+            VibrationEffect effect = VibrationEffect.createOneShot(
+                    milliseconds, VibrationEffect.DEFAULT_AMPLITUDE);
+            mEffects.add(effect);
+            applyLatency();
+            scheduleListener(milliseconds, vibrationId);
+        }
+
+        public void off() {
+        }
+
+        public void setAmplitude(int amplitude) {
+            mAmplitudes.add(amplitude);
+            applyLatency();
+        }
+
+        public int[] getSupportedEffects() {
+            return mSupportedEffects;
+        }
+
+        public int[] getSupportedPrimitives() {
+            return mSupportedPrimitives;
+        }
+
+        public long perform(long effect, long strength, long vibrationId) {
+            if (mSupportedEffects == null
+                    || Arrays.binarySearch(mSupportedEffects, (int) effect) < 0) {
+                return 0;
+            }
+            mEffects.add(new VibrationEffect.Prebaked((int) effect, false, (int) strength));
+            applyLatency();
+            scheduleListener(EFFECT_DURATION, vibrationId);
+            return EFFECT_DURATION;
+        }
+
+        public void compose(VibrationEffect.Composition.PrimitiveEffect[] effect,
+                long vibrationId) {
+            VibrationEffect.Composed composed = new VibrationEffect.Composed(Arrays.asList(effect));
+            mEffects.add(composed);
+            applyLatency();
+            long duration = EFFECT_DURATION * effect.length;
+            for (VibrationEffect.Composition.PrimitiveEffect e : effect) {
+                duration += e.delay;
+            }
+            scheduleListener(duration, vibrationId);
+        }
+
+        public void setExternalControl(boolean enabled) {
+        }
+
+        public long getCapabilities() {
+            return mCapabilities;
+        }
+
+        public void alwaysOnEnable(long id, long effect, long strength) {
+            VibrationEffect.Prebaked prebaked = new VibrationEffect.Prebaked((int) effect, false,
+                    (int) strength);
+            mEnabledAlwaysOnEffects.put(id, prebaked);
+        }
+
+        public void alwaysOnDisable(long id) {
+            mEnabledAlwaysOnEffects.remove(id);
+        }
+
+        private void applyLatency() {
+            try {
+                if (mLatency > 0) {
+                    Thread.sleep(mLatency);
+                }
+            } catch (InterruptedException e) {
+            }
+        }
+
+        private void scheduleListener(long vibrationDuration, long vibrationId) {
+            mHandler.postDelayed(() -> listener.onComplete(vibratorId, vibrationId),
+                    vibrationDuration);
+        }
+    }
+
+    public FakeVibratorControllerProvider(Looper looper) {
+        mHandler = new Handler(looper);
+        mNativeWrapper = new FakeNativeWrapper();
+    }
+
+    public VibratorController newVibratorController(
+            int vibratorId, OnVibrationCompleteListener listener) {
+        return new VibratorController(vibratorId, listener, mNativeWrapper);
+    }
+
+    /** Return {@code true} if this controller was initialized. */
+    public boolean isInitialized() {
+        return mNativeWrapper.isInitialized;
+    }
+
+    /**
+     * Disable fake vibrator hardware, mocking a state where the underlying service is unavailable.
+     */
+    public void disableVibrators() {
+        mIsAvailable = false;
+    }
+
+    /**
+     * Sets the latency this controller should fake for turning the vibrator hardware on or setting
+     * it's vibration amplitude.
+     */
+    public void setLatency(long millis) {
+        mLatency = millis;
+    }
+
+    /** Set the capabilities of the fake vibrator hardware. */
+    public void setCapabilities(int... capabilities) {
+        mCapabilities = Arrays.stream(capabilities).reduce(0, (a, b) -> a | b);
+    }
+
+    /** Set the effects supported by the fake vibrator hardware. */
+    public void setSupportedEffects(int... effects) {
+        if (effects != null) {
+            effects = Arrays.copyOf(effects, effects.length);
+            Arrays.sort(effects);
+        }
+        mSupportedEffects = effects;
+    }
+
+    /** Set the primitives supported by the fake vibrator hardware. */
+    public void setSupportedPrimitives(int... primitives) {
+        if (primitives != null) {
+            primitives = Arrays.copyOf(primitives, primitives.length);
+            Arrays.sort(primitives);
+        }
+        mSupportedPrimitives = primitives;
+    }
+
+    /**
+     * Return the amplitudes set by this controller, including zeroes for each time the vibrator was
+     * turned off.
+     */
+    public List<Integer> getAmplitudes() {
+        return new ArrayList<>(mAmplitudes);
+    }
+
+    /** Return list of {@link VibrationEffect} played by this controller, in order. */
+    public List<VibrationEffect> getEffects() {
+        return new ArrayList<>(mEffects);
+    }
+
+    /**
+     * Return the {@link VibrationEffect.Prebaked} effect enabled with given id, or {@code null} if
+     * missing or disabled.
+     */
+    @Nullable
+    public VibrationEffect.Prebaked getAlwaysOnEffect(int id) {
+        return mEnabledAlwaysOnEffects.get((long) id);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
index ac93ff6..28d313b 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
@@ -34,6 +34,7 @@
 import android.hardware.input.IInputDevicesChangedListener;
 import android.hardware.input.IInputManager;
 import android.hardware.input.InputManager;
+import android.os.CombinedVibrationEffect;
 import android.os.Handler;
 import android.os.Process;
 import android.os.VibrationAttributes;
@@ -66,6 +67,9 @@
     private static final String REASON = "some reason";
     private static final VibrationAttributes VIBRATION_ATTRIBUTES =
             new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build();
+    private static final VibrationEffect EFFECT = VibrationEffect.createOneShot(100, 255);
+    private static final CombinedVibrationEffect SYNCED_EFFECT =
+            CombinedVibrationEffect.createSynced(EFFECT);
 
     @Rule public MockitoRule rule = MockitoJUnit.rule();
 
@@ -227,10 +231,9 @@
 
     @Test
     public void vibrateIfAvailable_withNoInputDevice_returnsFalse() {
-        VibrationEffect effect = VibrationEffect.createOneShot(100, 255);
         assertFalse(mInputDeviceDelegate.isAvailable());
         assertFalse(mInputDeviceDelegate.vibrateIfAvailable(
-                UID, PACKAGE_NAME, effect, REASON, VIBRATION_ATTRIBUTES));
+                UID, PACKAGE_NAME, SYNCED_EFFECT, REASON, VIBRATION_ATTRIBUTES));
     }
 
     @Test
@@ -241,11 +244,10 @@
         when(mIInputManagerMock.getInputDevice(eq(2))).thenReturn(createInputDeviceWithVibrator(2));
         mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true);
 
-        VibrationEffect effect = VibrationEffect.createOneShot(100, 255);
         assertTrue(mInputDeviceDelegate.vibrateIfAvailable(
-                UID, PACKAGE_NAME, effect, REASON, VIBRATION_ATTRIBUTES));
-        verify(mIInputManagerMock).vibrate(eq(1), same(effect), any());
-        verify(mIInputManagerMock).vibrate(eq(2), same(effect), any());
+                UID, PACKAGE_NAME, SYNCED_EFFECT, REASON, VIBRATION_ATTRIBUTES));
+        verify(mIInputManagerMock).vibrate(eq(1), same(EFFECT), any());
+        verify(mIInputManagerMock).vibrate(eq(2), same(EFFECT), any());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
index 1a4ac07..82a6937 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -26,6 +26,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.os.CombinedVibrationEffect;
 import android.os.Handler;
 import android.os.IExternalVibratorService;
 import android.os.PowerManagerInternal;
@@ -131,6 +132,45 @@
     }
 
     @Test
+    public void scale_withCombined_resolvesAndScalesRecursively() {
+        setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
+                Vibrator.VIBRATION_INTENSITY_HIGH);
+        VibrationEffect prebaked = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+        VibrationEffect oneShot = VibrationEffect.createOneShot(10, 10);
+
+        CombinedVibrationEffect.Mono monoScaled = mVibrationScaler.scale(
+                CombinedVibrationEffect.createSynced(prebaked),
+                VibrationAttributes.USAGE_NOTIFICATION);
+        VibrationEffect.Prebaked prebakedScaled = (VibrationEffect.Prebaked) monoScaled.getEffect();
+        assertEquals(prebakedScaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+        CombinedVibrationEffect.Stereo stereoScaled = mVibrationScaler.scale(
+                CombinedVibrationEffect.startSynced()
+                        .addVibrator(1, prebaked)
+                        .addVibrator(2, oneShot)
+                        .combine(),
+                VibrationAttributes.USAGE_NOTIFICATION);
+        prebakedScaled = (VibrationEffect.Prebaked) stereoScaled.getEffects().get(1);
+        assertEquals(prebakedScaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+        VibrationEffect.OneShot oneshotScaled =
+                (VibrationEffect.OneShot) stereoScaled.getEffects().get(2);
+        assertTrue(oneshotScaled.getAmplitude() > 0);
+
+        CombinedVibrationEffect.Sequential sequentialScaled = mVibrationScaler.scale(
+                CombinedVibrationEffect.startSequential()
+                        .addNext(CombinedVibrationEffect.createSynced(prebaked))
+                        .addNext(CombinedVibrationEffect.createSynced(oneShot))
+                        .combine(),
+                VibrationAttributes.USAGE_NOTIFICATION);
+        monoScaled = (CombinedVibrationEffect.Mono) sequentialScaled.getEffects().get(0);
+        prebakedScaled = (VibrationEffect.Prebaked) monoScaled.getEffect();
+        assertEquals(prebakedScaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+        monoScaled = (CombinedVibrationEffect.Mono) sequentialScaled.getEffects().get(1);
+        oneshotScaled = (VibrationEffect.OneShot) monoScaled.getEffect();
+        assertTrue(oneshotScaled.getAmplitude() > 0);
+    }
+
+    @Test
     public void scale_withPrebaked_setsEffectStrengthBasedOnSettings() {
         setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
                 Vibrator.VIBRATION_INTENSITY_HIGH);
@@ -158,6 +198,28 @@
     }
 
     @Test
+    public void scale_withPrebakedAndFallback_resolvesAndScalesRecursively() {
+        setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
+                Vibrator.VIBRATION_INTENSITY_HIGH);
+        VibrationEffect.OneShot fallback2 = (VibrationEffect.OneShot) VibrationEffect.createOneShot(
+                10, VibrationEffect.DEFAULT_AMPLITUDE);
+        VibrationEffect.Prebaked fallback1 = new VibrationEffect.Prebaked(
+                VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_STRENGTH_MEDIUM, fallback2);
+        VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                VibrationEffect.EFFECT_STRENGTH_MEDIUM, fallback1);
+
+        VibrationEffect.Prebaked scaled = mVibrationScaler.scale(
+                effect, VibrationAttributes.USAGE_NOTIFICATION);
+        VibrationEffect.Prebaked scaledFallback1 =
+                (VibrationEffect.Prebaked) scaled.getFallbackEffect();
+        VibrationEffect.OneShot scaledFallback2 =
+                (VibrationEffect.OneShot) scaledFallback1.getFallbackEffect();
+        assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+        assertEquals(scaledFallback1.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+        assertTrue(scaledFallback2.getAmplitude() > 0);
+    }
+
+    @Test
     public void scale_withOneShotAndWaveform_resolvesAmplitude() {
         // No scale, default amplitude still resolved
         when(mVibratorMock.getDefaultRingVibrationIntensity())
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
new file mode 100644
index 0000000..bee7392
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2020 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.CombinedVibrationEffect;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.app.IBatteryStats;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests for {@link VibrationThread}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:VibrationThreadTest
+ */
+@Presubmit
+public class VibrationThreadTest {
+
+    private static final int TEST_TIMEOUT_MILLIS = 1_000;
+    private static final int UID = Process.ROOT_UID;
+    private static final int VIBRATOR_ID = 1;
+    private static final String PACKAGE_NAME = "package";
+    private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build();
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private VibrationThread.VibrationCallbacks mThreadCallbacks;
+    @Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks;
+    @Mock private IBinder mVibrationToken;
+    @Mock private IBatteryStats mIBatteryStatsMock;
+
+    private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
+    private PowerManager.WakeLock mWakeLock;
+    private TestLooper mTestLooper;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestLooper = new TestLooper();
+        mWakeLock = InstrumentationRegistry.getContext().getSystemService(
+                PowerManager.class).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*");
+
+        mockVibrators(VIBRATOR_ID);
+    }
+
+    @Test
+    public void vibrate_noVibrator_ignoresVibration() {
+        mVibratorProviders.clear();
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.IGNORED));
+    }
+
+    @Test
+    public void vibrate_missingVibrators_ignoresVibration() {
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSequential()
+                .addNext(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.IGNORED));
+    }
+
+    @Test
+    public void vibrate_singleVibratorOneShot_runsVibrationAndSetsAmplitude() throws Exception {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        assertEquals(Arrays.asList(expectedOneShot(10)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffects());
+        assertEquals(Arrays.asList(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+    }
+
+    @Test
+    public void vibrate_oneShotWithoutAmplitudeControl_runsVibrationWithDefaultAmplitude()
+            throws Exception {
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        assertEquals(Arrays.asList(expectedOneShot(10)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffects());
+        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
+    }
+
+    @Test
+    public void vibrate_singleVibratorWaveform_runsVibrationAndChangesAmplitudes()
+            throws Exception {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{5, 5, 5}, new int[]{1, 2, 3}, -1);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(15L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        assertEquals(Arrays.asList(expectedOneShot(15)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffects());
+        assertEquals(Arrays.asList(1, 2, 3), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+    }
+
+    @Test
+    public void vibrate_singleVibratorRepeatingWaveform_runsVibrationUntilThreadCancelled()
+            throws Exception {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        long vibrationId = 1;
+        int[] amplitudes = new int[]{1, 2, 3};
+        VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5, 5, 5}, amplitudes, 0);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+        Thread.sleep(35);
+        // Vibration still running after 2 cycles.
+        assertTrue(thread.isAlive());
+        assertTrue(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        thread.cancel();
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
+        verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        List<Integer> playedAmplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
+        assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+        assertFalse(playedAmplitudes.isEmpty());
+
+        for (int i = 0; i < playedAmplitudes.size(); i++) {
+            assertEquals(amplitudes[i % amplitudes.length], playedAmplitudes.get(i).intValue());
+        }
+    }
+
+    @Test
+    public void vibrate_singleVibratorPrebaked_runsVibration() throws Exception {
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_THUD);
+
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_THUD);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_THUD)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffects());
+    }
+
+    @Test
+    public void vibrate_singleVibratorPrebakedAndUnsupportedEffectWithFallback_runsFallback()
+            throws Exception {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        long vibrationId = 1;
+        VibrationEffect fallback = VibrationEffect.createOneShot(10, 100);
+        VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+                VibrationEffect.EFFECT_STRENGTH_STRONG, fallback);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        assertEquals(Arrays.asList(expectedOneShot(10)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffects());
+        assertEquals(Arrays.asList(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+    }
+
+    @Test
+    public void vibrate_singleVibratorPrebakedAndUnsupportedEffect_ignoresVibration()
+            throws Exception {
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
+        verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId),
+                eq(Vibration.Status.IGNORED_UNSUPPORTED));
+        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+    }
+
+    @Test
+    public void vibrate_singleVibratorComposed_runsVibration() throws Exception {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
+                .compose();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(40L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+        assertEquals(Arrays.asList(effect), mVibratorProviders.get(VIBRATOR_ID).getEffects());
+    }
+
+    @Test
+    public void vibrate_singleVibratorComposedAndNoCapability_ignoresVibration() throws Exception {
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
+                .compose();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
+        verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId),
+                eq(Vibration.Status.IGNORED_UNSUPPORTED));
+        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+    }
+
+    @Test
+    public void vibrate_singleVibratorCancelled_vibratorStopped() throws Exception {
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+        Thread.sleep(15);
+        // Vibration still running after 2 cycles.
+        assertTrue(thread.isAlive());
+        assertTrue(thread.getVibrators().get(1).isVibrating());
+
+        thread.binderDied();
+        waitForCompletion(thread);
+        assertFalse(thread.getVibrators().get(1).isVibrating());
+
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+    }
+
+    @Test
+    public void vibrate_multipleExistingAndMissingVibrators_vibratesOnlyExistingOnes()
+            throws Exception {
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_TICK);
+
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(VIBRATOR_ID, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
+                .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+        verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+        assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_TICK)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffects());
+    }
+
+    @Test
+    public void vibrate_multipleMono_runsSameEffectInAllVibrators() throws Exception {
+        mockVibrators(1, 2, 3);
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        mVibratorProviders.get(2).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        mVibratorProviders.get(3).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(1).isVibrating());
+        assertFalse(thread.getVibrators().get(2).isVibrating());
+        assertFalse(thread.getVibrators().get(3).isVibrating());
+
+        VibrationEffect expected = expectedPrebaked(VibrationEffect.EFFECT_CLICK);
+        assertEquals(Arrays.asList(expected), mVibratorProviders.get(1).getEffects());
+        assertEquals(Arrays.asList(expected), mVibratorProviders.get(2).getEffects());
+        assertEquals(Arrays.asList(expected), mVibratorProviders.get(3).getEffects());
+    }
+
+    @Test
+    public void vibrate_multipleStereo_runsVibrationOnRightVibrators() throws Exception {
+        mockVibrators(1, 2, 3, 4);
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+
+        long vibrationId = 1;
+        VibrationEffect composed = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                .compose();
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(2, VibrationEffect.createOneShot(10, 100))
+                .addVibrator(3, VibrationEffect.createWaveform(
+                        new long[]{10, 10}, new int[]{1, 2}, -1))
+                .addVibrator(4, composed)
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+        verify(mControllerCallbacks).onComplete(eq(4), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(1).isVibrating());
+        assertFalse(thread.getVibrators().get(2).isVibrating());
+        assertFalse(thread.getVibrators().get(3).isVibrating());
+        assertFalse(thread.getVibrators().get(4).isVibrating());
+
+        assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
+                mVibratorProviders.get(1).getEffects());
+        assertEquals(Arrays.asList(expectedOneShot(10)), mVibratorProviders.get(2).getEffects());
+        assertEquals(Arrays.asList(100), mVibratorProviders.get(2).getAmplitudes());
+        assertEquals(Arrays.asList(expectedOneShot(20)), mVibratorProviders.get(3).getEffects());
+        assertEquals(Arrays.asList(1, 2), mVibratorProviders.get(3).getAmplitudes());
+        assertEquals(Arrays.asList(composed), mVibratorProviders.get(4).getEffects());
+    }
+
+    @Test
+    public void vibrate_multipleSequential_runsVibrationInOrderWithDelays()
+            throws Exception {
+        mockVibrators(1, 2, 3);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        mVibratorProviders.get(3).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+
+        long vibrationId = 1;
+        VibrationEffect composed = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                .compose();
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSequential()
+                .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), /* delay= */ 50)
+                .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 50)
+                .addNext(2, composed, /* delay= */ 50)
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+        waitForCompletion(thread);
+        InOrder controllerVerifier = inOrder(mControllerCallbacks);
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+
+        InOrder batterVerifier = inOrder(mIBatteryStatsMock);
+        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(1).isVibrating());
+        assertFalse(thread.getVibrators().get(2).isVibrating());
+        assertFalse(thread.getVibrators().get(3).isVibrating());
+
+        assertEquals(Arrays.asList(expectedOneShot(10)), mVibratorProviders.get(1).getEffects());
+        assertEquals(Arrays.asList(100), mVibratorProviders.get(1).getAmplitudes());
+        assertEquals(Arrays.asList(composed), mVibratorProviders.get(2).getEffects());
+        assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
+                mVibratorProviders.get(3).getEffects());
+    }
+
+    @Test
+    public void vibrate_multipleWaveforms_playsWaveformsInParallel() throws Exception {
+        mockVibrators(1, 2, 3);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.createWaveform(
+                        new long[]{5, 10, 10}, new int[]{1, 2, 3}, -1))
+                .addVibrator(2, VibrationEffect.createWaveform(
+                        new long[]{20, 60}, new int[]{4, 5}, -1))
+                .addVibrator(3, VibrationEffect.createWaveform(
+                        new long[]{60}, new int[]{6}, -1))
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+        Thread.sleep(40);
+        // First waveform has finished.
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+        assertEquals(Arrays.asList(1, 2, 3), mVibratorProviders.get(1).getAmplitudes());
+        // Second waveform is halfway through.
+        assertEquals(Arrays.asList(4, 5), mVibratorProviders.get(2).getAmplitudes());
+        // Third waveform is almost ending.
+        assertEquals(Arrays.asList(6), mVibratorProviders.get(3).getAmplitudes());
+
+        waitForCompletion(thread);
+
+        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(80L));
+        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+        assertFalse(thread.getVibrators().get(1).isVibrating());
+        assertFalse(thread.getVibrators().get(2).isVibrating());
+        assertFalse(thread.getVibrators().get(3).isVibrating());
+
+        assertEquals(Arrays.asList(expectedOneShot(25)), mVibratorProviders.get(1).getEffects());
+        assertEquals(Arrays.asList(expectedOneShot(80)), mVibratorProviders.get(2).getEffects());
+        assertEquals(Arrays.asList(expectedOneShot(60)), mVibratorProviders.get(3).getEffects());
+        assertEquals(Arrays.asList(1, 2, 3), mVibratorProviders.get(1).getAmplitudes());
+        assertEquals(Arrays.asList(4, 5), mVibratorProviders.get(2).getAmplitudes());
+        assertEquals(Arrays.asList(6), mVibratorProviders.get(3).getAmplitudes());
+    }
+
+    @Test
+    public void vibrate_withWaveform_totalVibrationTimeRespected() {
+        int totalDuration = 10_000; // 10s
+        int stepDuration = 25; // 25ms
+
+        // 25% of the first waveform step will be spent on the native on() call.
+        // 25% of each waveform step will be spent on the native setAmplitude() call..
+        mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4);
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        int stepCount = totalDuration / stepDuration;
+        long[] timings = new long[stepCount];
+        int[] amplitudes = new int[stepCount];
+        Arrays.fill(timings, stepDuration);
+        Arrays.fill(amplitudes, VibrationEffect.DEFAULT_AMPLITUDE);
+        VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, -1);
+
+        long vibrationId = 1;
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        long startTime = SystemClock.elapsedRealtime();
+
+        waitForCompletion(thread, totalDuration + TEST_TIMEOUT_MILLIS);
+        long delay = Math.abs(SystemClock.elapsedRealtime() - startTime - totalDuration);
+
+        // Allow some delay for thread scheduling and callback triggering.
+        int maxDelay = (int) (0.05 * totalDuration); // < 5% of total duration
+        assertTrue("Waveform with perceived delay of " + delay + "ms,"
+                        + " expected less than " + maxDelay + "ms",
+                delay < maxDelay);
+    }
+
+    @Test
+    public void vibrate_multipleCancelled_allVibratorsStopped() throws Exception {
+        mockVibrators(1, 2, 3);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.createWaveform(
+                        new long[]{5, 10}, new int[]{1, 2}, 0))
+                .addVibrator(2, VibrationEffect.createWaveform(
+                        new long[]{20, 30}, new int[]{3, 4}, 0))
+                .addVibrator(3, VibrationEffect.createWaveform(
+                        new long[]{10, 40}, new int[]{5, 6}, 0))
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+        Thread.sleep(15);
+        assertTrue(thread.isAlive());
+        assertTrue(thread.getVibrators().get(1).isVibrating());
+        assertTrue(thread.getVibrators().get(2).isVibrating());
+        assertTrue(thread.getVibrators().get(3).isVibrating());
+
+        thread.cancel();
+        waitForCompletion(thread);
+        assertFalse(thread.getVibrators().get(1).isVibrating());
+        assertFalse(thread.getVibrators().get(2).isVibrating());
+        assertFalse(thread.getVibrators().get(3).isVibrating());
+
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+    }
+
+    @Test
+    public void vibrate_binderDied_cancelsVibration() throws Exception {
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+        Thread.sleep(15);
+        // Vibration still running after 2 cycles.
+        assertTrue(thread.isAlive());
+        assertTrue(thread.getVibrators().get(1).isVibrating());
+
+        thread.binderDied();
+        waitForCompletion(thread);
+
+        verify(mVibrationToken).linkToDeath(same(thread), eq(0));
+        verify(mVibrationToken).unlinkToDeath(same(thread), eq(0));
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+        assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+        assertFalse(thread.getVibrators().get(1).isVibrating());
+    }
+
+    private void mockVibrators(int... vibratorIds) {
+        for (int vibratorId : vibratorIds) {
+            mVibratorProviders.put(vibratorId,
+                    new FakeVibratorControllerProvider(mTestLooper.getLooper()));
+        }
+    }
+
+    private VibrationThread startThreadAndDispatcher(long vibrationId, VibrationEffect effect) {
+        return startThreadAndDispatcher(vibrationId, CombinedVibrationEffect.createSynced(effect));
+    }
+
+    private VibrationThread startThreadAndDispatcher(long vibrationId,
+            CombinedVibrationEffect effect) {
+        VibrationThread thread = new VibrationThread(createVibration(vibrationId, effect),
+                createVibratorControllers(), mWakeLock, mIBatteryStatsMock, mThreadCallbacks);
+        doAnswer(answer -> {
+            thread.vibratorComplete(answer.getArgument(0));
+            return null;
+        }).when(mControllerCallbacks).onComplete(anyInt(), eq(vibrationId));
+        mTestLooper.startAutoDispatch();
+        thread.start();
+        return thread;
+    }
+
+    private void waitForCompletion(VibrationThread thread) {
+        waitForCompletion(thread, TEST_TIMEOUT_MILLIS);
+    }
+
+    private void waitForCompletion(VibrationThread thread, long timeout) {
+        try {
+            thread.join(timeout);
+        } catch (InterruptedException e) {
+        }
+        assertFalse(thread.isAlive());
+        mTestLooper.dispatchAll();
+    }
+
+    private Vibration createVibration(long id, CombinedVibrationEffect effect) {
+        return new Vibration(mVibrationToken, (int) id, effect, ATTRS, UID, PACKAGE_NAME, "reason");
+    }
+
+    private SparseArray<VibratorController> createVibratorControllers() {
+        SparseArray<VibratorController> array = new SparseArray<>();
+        for (Map.Entry<Integer, FakeVibratorControllerProvider> e : mVibratorProviders.entrySet()) {
+            int id = e.getKey();
+            array.put(id, e.getValue().newVibratorController(id, mControllerCallbacks));
+        }
+        return array;
+    }
+
+    private VibrationEffect expectedOneShot(long millis) {
+        return VibrationEffect.createOneShot(millis, VibrationEffect.DEFAULT_AMPLITUDE);
+    }
+
+    private VibrationEffect expectedPrebaked(int effectId) {
+        return new VibrationEffect.Prebaked(effectId, false,
+                VibrationEffect.EFFECT_STRENGTH_MEDIUM);
+    }
+}