Use vibrator manager controller in VibratorManagerService

Add native methods to load manager capabilities and coordinate synced
vibrations in VibrationThread.

Bug: 167946816
Bug: 167946760
Test: VibratorManagerTest, VibrationThreadTest
Change-Id: I54dad9420be1e8cddae9023f8438581d3f553412
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 37d2cdc..96cfe02 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -103,7 +103,7 @@
         "android.hardware.gnss-V1-java",
         "android.hardware.power-V1-java",
         "android.hardware.power-V1.0-java",
-        "android.hardware.vibrator-V1-java",
+        "android.hardware.vibrator-V2-java",
         "android.net.ipsec.ike.stubs.module_lib",
         "app-compat-annotations",
         "framework-tethering.stubs.module_lib",
diff --git a/services/core/java/com/android/server/VibratorManagerService.java b/services/core/java/com/android/server/VibratorManagerService.java
index 6a816af..e7e5d67 100644
--- a/services/core/java/com/android/server/VibratorManagerService.java
+++ b/services/core/java/com/android/server/VibratorManagerService.java
@@ -111,6 +111,7 @@
     private final AppOpsManager mAppOps;
     private final NativeWrapper mNativeWrapper;
     private final VibratorManagerRecords mVibratorManagerRecords;
+    private final long mCapabilities;
     private final int[] mVibratorIds;
     private final SparseArray<VibratorController> mVibrators;
     private final VibrationCallbacks mVibrationCallbacks = new VibrationCallbacks();
@@ -127,18 +128,28 @@
     private VibrationScaler mVibrationScaler;
     private InputDeviceDelegate mInputDeviceDelegate;
 
-    static native long nativeInit();
+    static native long nativeInit(OnSyncedVibrationCompleteListener listener);
 
     static native long nativeGetFinalizer();
 
+    static native long nativeGetCapabilities(long nativeServicePtr);
+
     static native int[] nativeGetVibratorIds(long nativeServicePtr);
 
+    static native boolean nativePrepareSynced(long nativeServicePtr, int[] vibratorIds);
+
+    static native boolean nativeTriggerSynced(long nativeServicePtr, long vibrationId);
+
+    static native void nativeCancelSynced(long nativeServicePtr);
+
     @VisibleForTesting
     VibratorManagerService(Context context, Injector injector) {
         mContext = context;
         mHandler = injector.createHandler(Looper.myLooper());
+
+        VibrationCompleteListener listener = new VibrationCompleteListener(this);
         mNativeWrapper = injector.getNativeWrapper();
-        mNativeWrapper.init();
+        mNativeWrapper.init(listener);
 
         int dumpLimit = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_previousVibrationsDumpLimit);
@@ -153,6 +164,7 @@
         mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*");
         mWakeLock.setReferenceCounted(true);
 
+        mCapabilities = mNativeWrapper.getCapabilities();
         int[] vibratorIds = mNativeWrapper.getVibratorIds();
         if (vibratorIds == null) {
             mVibratorIds = new int[0];
@@ -161,11 +173,17 @@
             // Keep original vibrator id order, which might be meaningful.
             mVibratorIds = vibratorIds;
             mVibrators = new SparseArray<>(mVibratorIds.length);
-            VibrationCompleteListener listener = new VibrationCompleteListener(this);
             for (int vibratorId : vibratorIds) {
                 mVibrators.put(vibratorId, injector.createVibratorController(vibratorId, listener));
             }
         }
+
+        // Reset the hardware to a default state, in case this is a runtime restart instead of a
+        // fresh boot.
+        mNativeWrapper.cancelSynced();
+        for (int i = 0; i < mVibrators.size(); i++) {
+            mVibrators.valueAt(i).off();
+        }
     }
 
     /** Finish initialization at boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}. */
@@ -502,6 +520,17 @@
         }
     }
 
+    private void onSyncedVibrationComplete(long vibrationId) {
+        synchronized (mLock) {
+            if (mCurrentVibration != null && mCurrentVibration.getVibration().id == vibrationId) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread");
+                }
+                mCurrentVibration.syncedVibrationComplete();
+            }
+        }
+    }
+
     private void onVibrationComplete(int vibratorId, long vibrationId) {
         synchronized (mLock) {
             if (mCurrentVibration != null && mCurrentVibration.getVibration().id == vibrationId) {
@@ -839,13 +868,22 @@
     private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks {
 
         @Override
-        public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) {
-            // TODO(b/167946816): call IVibratorManager to prepare
+        public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) {
+            if ((mCapabilities & requiredCapabilities) != requiredCapabilities) {
+                // This sync step requires capabilities this device doesn't have, skipping sync...
+                return false;
+            }
+            return mNativeWrapper.prepareSynced(vibratorIds);
         }
 
         @Override
-        public void triggerSyncedVibration(long vibrationId) {
-            // TODO(b/167946816): call IVibratorManager to trigger
+        public boolean triggerSyncedVibration(long vibrationId) {
+            return mNativeWrapper.triggerSynced(vibrationId);
+        }
+
+        @Override
+        public void cancelSyncedVibration() {
+            mNativeWrapper.cancelSynced();
         }
 
         @Override
@@ -868,12 +906,19 @@
         }
     }
 
+    /** Listener for synced vibration completion callbacks from native. */
+    @VisibleForTesting
+    public interface OnSyncedVibrationCompleteListener {
+
+        /** Callback triggered when synced vibration is complete. */
+        void onComplete(long vibrationId);
+    }
+
     /**
-     * Implementation of {@link VibratorController.OnVibrationCompleteListener} with a weak
-     * reference to this service.
+     * Implementation of listeners to native vibrators with a weak reference to this service.
      */
     private static final class VibrationCompleteListener implements
-            VibratorController.OnVibrationCompleteListener {
+            VibratorController.OnVibrationCompleteListener, OnSyncedVibrationCompleteListener {
         private WeakReference<VibratorManagerService> mServiceRef;
 
         VibrationCompleteListener(VibratorManagerService service) {
@@ -881,6 +926,14 @@
         }
 
         @Override
+        public void onComplete(long vibrationId) {
+            VibratorManagerService service = mServiceRef.get();
+            if (service != null) {
+                service.onSyncedVibrationComplete(vibrationId);
+            }
+        }
+
+        @Override
         public void onComplete(int vibratorId, long vibrationId) {
             VibratorManagerService service = mServiceRef.get();
             if (service != null) {
@@ -952,9 +1005,9 @@
         private long mNativeServicePtr = 0;
 
         /** Returns native pointer to newly created controller and connects with HAL service. */
-        public void init() {
-            mNativeServicePtr = VibratorManagerService.nativeInit();
-            long finalizerPtr = VibratorManagerService.nativeGetFinalizer();
+        public void init(OnSyncedVibrationCompleteListener listener) {
+            mNativeServicePtr = nativeInit(listener);
+            long finalizerPtr = nativeGetFinalizer();
 
             if (finalizerPtr != 0) {
                 NativeAllocationRegistry registry =
@@ -964,9 +1017,29 @@
             }
         }
 
+        /** Returns manager capabilities. */
+        public long getCapabilities() {
+            return nativeGetCapabilities(mNativeServicePtr);
+        }
+
         /** Returns vibrator ids. */
         public int[] getVibratorIds() {
-            return VibratorManagerService.nativeGetVibratorIds(mNativeServicePtr);
+            return nativeGetVibratorIds(mNativeServicePtr);
+        }
+
+        /** Prepare vibrators for triggering vibrations in sync. */
+        public boolean prepareSynced(@NonNull int[] vibratorIds) {
+            return nativePrepareSynced(mNativeServicePtr, vibratorIds);
+        }
+
+        /** Trigger prepared synced vibration. */
+        public boolean triggerSynced(long vibrationId) {
+            return nativeTriggerSynced(mNativeServicePtr, vibrationId);
+        }
+
+        /** Cancel prepared synced vibration. */
+        public void cancelSynced() {
+            nativeCancelSynced(mNativeServicePtr);
         }
     }
 
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index 026eb63..2ac365d 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -119,11 +119,17 @@
     private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks {
 
         @Override
-        public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) {
+        public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) {
+            return false;
         }
 
         @Override
-        public void triggerSyncedVibration(long vibrationId) {
+        public boolean triggerSyncedVibration(long vibrationId) {
+            return false;
+        }
+
+        @Override
+        public void cancelSyncedVibration() {
         }
 
         @Override
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 4f2fc86..bee66637 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -17,6 +17,7 @@
 package com.android.server.vibrator;
 
 import android.annotation.Nullable;
+import android.hardware.vibrator.IVibratorManager;
 import android.os.CombinedVibrationEffect;
 import android.os.IBinder;
 import android.os.PowerManager;
@@ -65,10 +66,13 @@
          *                             IVibratorManager.CAP_MIXED_TRIGGER_*.
          * @param vibratorIds          The id of the vibrators to be prepared.
          */
-        void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds);
+        boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds);
 
         /** Callback triggered after synchronized vibrations were prepared. */
-        void triggerSyncedVibration(long vibrationId);
+        boolean triggerSyncedVibration(long vibrationId);
+
+        /** Callback triggered to cancel a prepared synced vibration. */
+        void cancelSyncedVibration();
 
         /** Callback triggered when vibration thread is complete. */
         void onVibrationEnded(long vibrationId, Vibration.Status status);
@@ -146,6 +150,20 @@
         }
     }
 
+    /** Notify current vibration that a synced step has completed. */
+    public void syncedVibrationComplete() {
+        synchronized (mLock) {
+            if (DEBUG) {
+                Slog.d(TAG, "Synced vibration complete reported by vibrator manager");
+            }
+            if (mCurrentVibrateStep != null) {
+                for (int i = 0; i < mVibrators.size(); i++) {
+                    mCurrentVibrateStep.vibratorComplete(mVibrators.keyAt(i));
+                }
+            }
+        }
+    }
+
     /** Notify current vibration that a step has completed on given vibrator. */
     public void vibratorComplete(int vibratorId) {
         synchronized (mLock) {
@@ -530,7 +548,7 @@
     /** Represent a synchronized vibration step on multiple vibrators. */
     private final class SyncedVibrateStep implements VibrateStep {
         private final SparseArray<VibrationEffect> mEffects;
-        private final int mRequiredCapabilities;
+        private final long mRequiredCapabilities;
         private final int[] mVibratorIds;
 
         @GuardedBy("mLock")
@@ -539,8 +557,7 @@
         SyncedVibrateStep(SparseArray<VibrationEffect> effects) {
             mEffects = effects;
             mActiveVibratorCounter = mEffects.size();
-            // TODO(b/159207608): Calculate required capabilities for syncing this step.
-            mRequiredCapabilities = 0;
+            mRequiredCapabilities = calculateRequiredSyncCapabilities(effects);
             mVibratorIds = new int[effects.size()];
             for (int i = 0; i < effects.size(); i++) {
                 mVibratorIds[i] = effects.keyAt(i);
@@ -574,18 +591,21 @@
         @Override
         public Vibration.Status play() {
             Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SyncedVibrateStep");
-            long timeout = -1;
+            long duration = -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);
+                duration = startVibratingSynced(startTime, nextSteps);
 
+                if (duration <= 0) {
+                    // Vibrate step failed, vibrator could not be turned on for this step.
+                    return Vibration.Status.IGNORED;
+                }
+
+                noteVibratorOn(duration);
                 while (!nextSteps.isEmpty()) {
                     AmplitudeStep step = nextSteps.poll();
                     if (!waitUntil(step.startTime)) {
@@ -607,7 +627,7 @@
                 synchronized (mLock) {
                     // All OneShot and Waveform effects have finished. Just wait for the other
                     // effects to end via native callbacks before finishing this synced step.
-                    final long wakeUpTime = startTime + timeout + CALLBACKS_EXTRA_TIMEOUT;
+                    final long wakeUpTime = startTime + duration + CALLBACKS_EXTRA_TIMEOUT;
                     if (mActiveVibratorCounter <= 0 || waitForVibrationComplete(this, wakeUpTime)) {
                         return Vibration.Status.FINISHED;
                     }
@@ -617,7 +637,7 @@
                     return mForceStop ? Vibration.Status.CANCELLED : Vibration.Status.FINISHED;
                 }
             } finally {
-                if (timeout > 0) {
+                if (duration > 0) {
                     noteVibratorOff();
                 }
                 if (DEBUG) {
@@ -628,14 +648,48 @@
         }
 
         /**
+         * Starts playing effects on designated vibrators, in sync.
+         *
+         * @return A positive duration, in millis, to wait for the completion of this effect.
+         * Non-positive values indicate the vibrator has ignored this effect. Repeating waveform
+         * returns the duration of a single run to be used as timeout for callbacks.
+         */
+        private long startVibratingSynced(long startTime, PriorityQueue<AmplitudeStep> nextSteps) {
+            // This synchronization of vibrators should be executed one at a time, even if we are
+            // vibrating different sets of vibrators in parallel. The manager can only prepareSynced
+            // one set of vibrators at a time.
+            synchronized (mLock) {
+                boolean hasPrepared = false;
+                boolean hasTriggered = false;
+                try {
+                    hasPrepared = mCallbacks.prepareSyncedVibration(mRequiredCapabilities,
+                            mVibratorIds);
+                    long timeout = startVibrating(startTime, nextSteps);
+
+                    // Check if preparation was successful, otherwise devices area already vibrating
+                    if (hasPrepared) {
+                        hasTriggered = mCallbacks.triggerSyncedVibration(mVibration.id);
+                    }
+                    return timeout;
+                } finally {
+                    if (hasPrepared && !hasTriggered) {
+                        mCallbacks.cancelSyncedVibration();
+                        return 0;
+                    }
+                }
+            }
+        }
+
+        /**
          * 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.
+         * @return A positive duration, in millis, to wait for the completion of this effect.
+         * Non-positive values indicate the vibrator has ignored this effect. Repeating waveform
+         * returns the duration of a single run to be used as timeout for callbacks.
          */
         private long startVibrating(long startTime, PriorityQueue<AmplitudeStep> nextSteps) {
             long maxDuration = 0;
@@ -651,9 +705,9 @@
         /**
          * 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.
+         * @return A positive duration, in millis, to wait for the completion of this effect.
+         * Non-positive values indicate the vibrator has ignored this effect. Repeating waveform
+         * 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) {
@@ -698,6 +752,49 @@
                 }
             }
         }
+
+        /**
+         * Return all capabilities required from the {@link IVibratorManager} to prepare and
+         * trigger all given effects in sync.
+         *
+         * @return {@link IVibratorManager#CAP_SYNC} together with all required
+         * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities.
+         */
+        private long calculateRequiredSyncCapabilities(SparseArray<VibrationEffect> effects) {
+            long prepareCap = 0;
+            for (int i = 0; i < effects.size(); i++) {
+                VibrationEffect effect = effects.valueAt(i);
+                if (effect instanceof VibrationEffect.OneShot
+                        || effect instanceof VibrationEffect.Waveform) {
+                    prepareCap |= IVibratorManager.CAP_PREPARE_ON;
+                } else if (effect instanceof VibrationEffect.Prebaked) {
+                    prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM;
+                } else if (effect instanceof VibrationEffect.Composed) {
+                    prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE;
+                }
+            }
+            int triggerCap = 0;
+            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_ON)) {
+                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_ON;
+            }
+            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_PERFORM)) {
+                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
+            }
+            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_COMPOSE)) {
+                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
+            }
+            return IVibratorManager.CAP_SYNC | prepareCap | triggerCap;
+        }
+
+        /**
+         * Return true if {@code prepareCapabilities} contains this {@code capability} mixed with
+         * different ones, requiring a mixed trigger capability from the vibrator manager for
+         * syncing all effects.
+         */
+        private boolean requireMixedTriggerCapability(long prepareCapabilities, long capability) {
+            return (prepareCapabilities & capability) != 0
+                    && (prepareCapabilities & ~capability) != 0;
+        }
     }
 
     /** Represent a step to set amplitude on a single vibrator. */
@@ -794,12 +891,12 @@
                 // Waveform has ended, no more steps to run.
                 return null;
             }
-            long nextWakeUpTime = startTime + waveform.getTimings()[currentIndex];
+            long nextStartTime = startTime + waveform.getTimings()[currentIndex];
             int nextIndex = currentIndex + 1;
             if (nextIndex >= waveform.getTimings().length) {
                 nextIndex = waveform.getRepeatIndex();
             }
-            return new AmplitudeStep(vibratorId, waveform, nextIndex, nextWakeUpTime,
+            return new AmplitudeStep(vibratorId, waveform, nextIndex, nextStartTime,
                     nextVibratorStopTime());
         }
 
diff --git a/services/core/jni/com_android_server_VibratorManagerService.cpp b/services/core/jni/com_android_server_VibratorManagerService.cpp
index 71de9bd..5dbb71a 100644
--- a/services/core/jni/com_android_server_VibratorManagerService.cpp
+++ b/services/core/jni/com_android_server_VibratorManagerService.cpp
@@ -24,27 +24,47 @@
 #include <utils/Log.h>
 #include <utils/misc.h>
 
-#include <vibratorservice/VibratorManagerHalWrapper.h>
+#include <vibratorservice/VibratorManagerHalController.h>
 
 #include "com_android_server_VibratorManagerService.h"
 
 namespace android {
 
+static JavaVM* sJvm = nullptr;
+static jmethodID sMethodIdOnComplete;
 static std::mutex gManagerMutex;
-static vibrator::ManagerHalWrapper* gManager GUARDED_BY(gManagerMutex) = nullptr;
+static vibrator::ManagerHalController* gManager GUARDED_BY(gManagerMutex) = nullptr;
 
 class NativeVibratorManagerService {
 public:
-    NativeVibratorManagerService() : mHal(std::make_unique<vibrator::LegacyManagerHalWrapper>()) {}
-    ~NativeVibratorManagerService() = default;
+    NativeVibratorManagerService(JNIEnv* env, jobject callbackListener)
+          : mHal(std::make_unique<vibrator::ManagerHalController>()),
+            mCallbackListener(env->NewGlobalRef(callbackListener)) {
+        LOG_ALWAYS_FATAL_IF(mHal == nullptr, "Unable to find reference to vibrator manager hal");
+        LOG_ALWAYS_FATAL_IF(mCallbackListener == nullptr,
+                            "Unable to create global reference to vibration callback handler");
+    }
 
-    vibrator::ManagerHalWrapper* hal() const { return mHal.get(); }
+    ~NativeVibratorManagerService() {
+        auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
+        jniEnv->DeleteGlobalRef(mCallbackListener);
+    }
+
+    vibrator::ManagerHalController* hal() const { return mHal.get(); }
+
+    std::function<void()> createCallback(jlong vibrationId) {
+        return [vibrationId, this]() {
+            auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
+            jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, vibrationId);
+        };
+    }
 
 private:
-    const std::unique_ptr<vibrator::ManagerHalWrapper> mHal;
+    const std::unique_ptr<vibrator::ManagerHalController> mHal;
+    const jobject mCallbackListener;
 };
 
-vibrator::ManagerHalWrapper* android_server_VibratorManagerService_getManager() {
+vibrator::ManagerHalController* android_server_VibratorManagerService_getManager() {
     std::lock_guard<std::mutex> lock(gManagerMutex);
     return gManager;
 }
@@ -58,9 +78,9 @@
     }
 }
 
-static jlong nativeInit(JNIEnv* /* env */, jclass /* clazz */) {
+static jlong nativeInit(JNIEnv* env, jclass /* clazz */, jobject callbackListener) {
     std::unique_ptr<NativeVibratorManagerService> service =
-            std::make_unique<NativeVibratorManagerService>();
+            std::make_unique<NativeVibratorManagerService>(env, callbackListener);
     {
         std::lock_guard<std::mutex> lock(gManagerMutex);
         gManager = service->hal();
@@ -72,6 +92,17 @@
     return static_cast<jlong>(reinterpret_cast<uintptr_t>(&destroyNativeService));
 }
 
+static jlong nativeGetCapabilities(JNIEnv* env, jclass /* clazz */, jlong servicePtr) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeGetCapabilities failed because native service was not initialized");
+        return 0;
+    }
+    auto result = service->hal()->getCapabilities();
+    return result.isOk() ? static_cast<jlong>(result.value()) : 0;
+}
+
 static jintArray nativeGetVibratorIds(JNIEnv* env, jclass /* clazz */, jlong servicePtr) {
     NativeVibratorManagerService* service =
             reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
@@ -89,13 +120,61 @@
     return ids;
 }
 
+static jboolean nativePrepareSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr,
+                                    jintArray vibratorIds) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativePrepareSynced failed because native service was not initialized");
+        return JNI_FALSE;
+    }
+    jsize size = env->GetArrayLength(vibratorIds);
+    std::vector<int32_t> ids(size);
+    env->GetIntArrayRegion(vibratorIds, 0, size, reinterpret_cast<jint*>(ids.data()));
+    return service->hal()->prepareSynced(ids).isOk() ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean nativeTriggerSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr,
+                                    jlong vibrationId) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeTriggerSynced failed because native service was not initialized");
+        return JNI_FALSE;
+    }
+    auto callback = service->createCallback(vibrationId);
+    return service->hal()->triggerSynced(callback).isOk() ? JNI_TRUE : JNI_FALSE;
+}
+
+static void nativeCancelSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeCancelSynced failed because native service was not initialized");
+        return;
+    }
+    service->hal()->cancelSynced();
+}
+
 static const JNINativeMethod method_table[] = {
-        {"nativeInit", "()J", (void*)nativeInit},
+        {"nativeInit",
+         "(Lcom/android/server/VibratorManagerService$OnSyncedVibrationCompleteListener;)J",
+         (void*)nativeInit},
         {"nativeGetFinalizer", "()J", (void*)nativeGetFinalizer},
+        {"nativeGetCapabilities", "(J)J", (void*)nativeGetCapabilities},
         {"nativeGetVibratorIds", "(J)[I", (void*)nativeGetVibratorIds},
+        {"nativePrepareSynced", "(J[I)Z", (void*)nativePrepareSynced},
+        {"nativeTriggerSynced", "(JJ)Z", (void*)nativeTriggerSynced},
+        {"nativeCancelSynced", "(J)V", (void*)nativeCancelSynced},
 };
 
-int register_android_server_VibratorManagerService(JNIEnv* env) {
+int register_android_server_VibratorManagerService(JavaVM* jvm, JNIEnv* env) {
+    sJvm = jvm;
+    auto listenerClassName =
+            "com/android/server/VibratorManagerService$OnSyncedVibrationCompleteListener";
+    jclass listenerClass = FindClassOrDie(env, listenerClassName);
+    sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(J)V");
+
     return jniRegisterNativeMethods(env, "com/android/server/VibratorManagerService", method_table,
                                     NELEM(method_table));
 }
diff --git a/services/core/jni/com_android_server_VibratorManagerService.h b/services/core/jni/com_android_server_VibratorManagerService.h
index 3f2a322..22950c5 100644
--- a/services/core/jni/com_android_server_VibratorManagerService.h
+++ b/services/core/jni/com_android_server_VibratorManagerService.h
@@ -17,11 +17,11 @@
 #ifndef _ANDROID_SERVER_VIBRATOR_MANAGER_SERVICE_H
 #define _ANDROID_SERVER_VIBRATOR_MANAGER_SERVICE_H
 
-#include <vibratorservice/VibratorManagerHalWrapper.h>
+#include <vibratorservice/VibratorManagerHalController.h>
 
 namespace android {
 
-extern vibrator::ManagerHalWrapper* android_server_VibratorManagerService_getManager();
+extern vibrator::ManagerHalController* android_server_VibratorManagerService_getManager();
 
 } // namespace android
 
diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
index 7ed3748..4e47984 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
@@ -77,7 +77,7 @@
     if (vibratorId < 0) {
         return std::move(std::make_unique<vibrator::HalController>());
     }
-    vibrator::ManagerHalWrapper* manager = android_server_VibratorManagerService_getManager();
+    vibrator::ManagerHalController* manager = android_server_VibratorManagerService_getManager();
     if (manager == nullptr) {
         return nullptr;
     }
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index c5394f3..34f6048 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -39,7 +39,7 @@
 int register_android_server_UsbHostManager(JNIEnv* env);
 int register_android_server_vr_VrManagerService(JNIEnv* env);
 int register_android_server_vibrator_VibratorController(JavaVM* vm, JNIEnv* env);
-int register_android_server_VibratorManagerService(JNIEnv* env);
+int register_android_server_VibratorManagerService(JavaVM* vm, JNIEnv* env);
 int register_android_server_location_GnssLocationProvider(JNIEnv* env);
 int register_android_server_devicepolicy_CryptoTestHelper(JNIEnv*);
 int register_android_server_tv_TvUinputBridge(JNIEnv* env);
@@ -90,7 +90,7 @@
     register_android_server_UsbHostManager(env);
     register_android_server_vr_VrManagerService(env);
     register_android_server_vibrator_VibratorController(vm, env);
-    register_android_server_VibratorManagerService(env);
+    register_android_server_VibratorManagerService(vm, env);
     register_android_server_SystemServer(env);
     register_android_server_location_GnssLocationProvider(env);
     register_android_server_devicepolicy_CryptoTestHelper(env);
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 7a0cb8e..31e2b64 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -61,7 +61,7 @@
     libs: [
         "android.hardware.power-V1-java",
         "android.hardware.tv.cec-V1.0-java",
-        "android.hardware.vibrator-V1-java",
+        "android.hardware.vibrator-V2-java",
         "android.hidl.manager-V1.0-java",
         "android.test.mock",
         "android.test.base",
diff --git a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
index f7b2492..f0d7006 100644
--- a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertTrue;
 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.Mockito.atLeastOnce;
@@ -32,6 +33,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -44,6 +46,7 @@
 import android.hardware.input.IInputManager;
 import android.hardware.input.InputManager;
 import android.hardware.vibrator.IVibrator;
+import android.hardware.vibrator.IVibratorManager;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.os.CombinedVibrationEffect;
@@ -204,7 +207,7 @@
     public void createService_initializesNativeManagerServiceAndVibrators() {
         mockVibrators(1, 2);
         createService();
-        verify(mNativeWrapperMock).init();
+        verify(mNativeWrapperMock).init(any());
         assertTrue(mVibratorProviders.get(1).isInitialized());
         assertTrue(mVibratorProviders.get(2).isInitialized());
     }
@@ -557,8 +560,6 @@
     @Test
     public void vibrate_withNativeCallbackTriggered_finishesVibration() throws Exception {
         mockVibrators(1);
-        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS,
-                IVibrator.CAP_AMPLITUDE_CONTROL);
         mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
         VibratorManagerService service = createService();
         // The native callback will be dispatched manually in this test.
@@ -577,6 +578,139 @@
         assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
     }
 
+    @Test
+    public void vibrate_withTriggerCallback_finishesVibration() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_COMPOSE);
+        mockVibrators(1, 2);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        VibratorManagerService service = createService();
+        // The native callback will be dispatched manually in this test.
+        mTestLooper.stopAutoDispatchAndIgnoreExceptions();
+
+        ArgumentCaptor<VibratorManagerService.OnSyncedVibrationCompleteListener> listenerCaptor =
+                ArgumentCaptor.forClass(
+                        VibratorManagerService.OnSyncedVibrationCompleteListener.class);
+        verify(mNativeWrapperMock).init(listenerCaptor.capture());
+
+        // Mock trigger callback on registered listener.
+        when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true);
+        when(mNativeWrapperMock.triggerSynced(anyLong())).then(answer -> {
+            listenerCaptor.getValue().onComplete(answer.getArgument(0));
+            return true;
+        });
+
+        VibrationEffect composed = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100)
+                .compose();
+        CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(composed);
+
+        // Wait for vibration to start, it should finish right away with trigger callback.
+        vibrate(service, effect, ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait until callback is triggered.
+        assertTrue(waitUntil(s -> !listenerCaptor.getAllValues().isEmpty(), service,
+                TEST_TIMEOUT_MILLIS));
+
+        verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2}));
+        verify(mNativeWrapperMock).triggerSynced(anyLong());
+        assertEquals(Arrays.asList(composed), mVibratorProviders.get(1).getEffects());
+        assertEquals(Arrays.asList(composed), mVibratorProviders.get(2).getEffects());
+    }
+
+    @Test
+    public void vibrate_withMultipleVibratorsAndCapabilities_prepareAndTriggerCalled()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_PERFORM,
+                IVibratorManager.CAP_PREPARE_COMPOSE, IVibratorManager.CAP_MIXED_TRIGGER_PERFORM,
+                IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE);
+        mockVibrators(1, 2);
+        when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true);
+        when(mNativeWrapperMock.triggerSynced(anyLong())).thenReturn(true);
+        FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1);
+        fakeVibrator1.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        VibratorManagerService service = createService();
+
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(2, VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .compose())
+                .combine();
+        vibrate(service, effect, ALARM_ATTRS);
+        assertTrue(waitUntil(s -> !fakeVibrator1.getEffects().isEmpty(), service,
+                TEST_TIMEOUT_MILLIS));
+
+        verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2}));
+        verify(mNativeWrapperMock).triggerSynced(anyLong());
+        verify(mNativeWrapperMock).cancelSynced(); // Trigger on service creation only.
+    }
+
+    @Test
+    public void vibrate_withMultipleVibratorsWithoutCapabilities_skipPrepareAndTrigger()
+            throws Exception {
+        // Missing CAP_MIXED_TRIGGER_ON and CAP_MIXED_TRIGGER_PERFORM.
+        mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_ON,
+                IVibratorManager.CAP_PREPARE_PERFORM);
+        mockVibrators(1, 2);
+        FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1);
+        fakeVibrator1.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createService();
+
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(2, VibrationEffect.createOneShot(10, 100))
+                .combine();
+        vibrate(service, effect, ALARM_ATTRS);
+        assertTrue(waitUntil(s -> !fakeVibrator1.getEffects().isEmpty(), service,
+                TEST_TIMEOUT_MILLIS));
+
+        verify(mNativeWrapperMock, never()).prepareSynced(any());
+        verify(mNativeWrapperMock, never()).triggerSynced(anyLong());
+        verify(mNativeWrapperMock).cancelSynced(); // Trigger on service creation only.
+    }
+
+    @Test
+    public void vibrate_withMultipleVibratorsPrepareFailed_skipTrigger() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_ON);
+        mockVibrators(1, 2);
+        when(mNativeWrapperMock.prepareSynced(any())).thenReturn(false);
+        VibratorManagerService service = createService();
+
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.createOneShot(10, 50))
+                .addVibrator(2, VibrationEffect.createOneShot(10, 100))
+                .combine();
+        vibrate(service, effect, ALARM_ATTRS);
+        assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getEffects().isEmpty(), service,
+                TEST_TIMEOUT_MILLIS));
+
+        verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2}));
+        verify(mNativeWrapperMock, never()).triggerSynced(anyLong());
+        verify(mNativeWrapperMock).cancelSynced(); // Trigger on service creation only.
+    }
+
+    @Test
+    public void vibrate_withMultipleVibratorsTriggerFailed_cancelPreparedSynced() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_ON);
+        mockVibrators(1, 2);
+        when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true);
+        when(mNativeWrapperMock.triggerSynced(anyLong())).thenReturn(false);
+        VibratorManagerService service = createService();
+
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.createOneShot(10, 50))
+                .addVibrator(2, VibrationEffect.createOneShot(10, 100))
+                .combine();
+        vibrate(service, effect, ALARM_ATTRS);
+        assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getEffects().isEmpty(), service,
+                TEST_TIMEOUT_MILLIS));
+
+        verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2}));
+        verify(mNativeWrapperMock).triggerSynced(anyLong());
+        verify(mNativeWrapperMock, times(2)).cancelSynced(); // Trigger on service creation too.
+    }
 
     @Test
     public void vibrate_withIntensitySettings_appliesSettingsToScaleVibrations() throws Exception {
@@ -682,6 +816,11 @@
         assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
     }
 
+    private void mockCapabilities(long... capabilities) {
+        when(mNativeWrapperMock.getCapabilities()).thenReturn(
+                Arrays.stream(capabilities).reduce(0, (a, b) -> a | b));
+    }
+
     private void mockVibrators(int... vibratorIds) {
         for (int vibratorId : vibratorIds) {
             mVibratorProviders.put(vibratorId,
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index 7d20879..3ff8e76 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
@@ -27,8 +28,10 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.hardware.vibrator.IVibrator;
+import android.hardware.vibrator.IVibratorManager;
 import android.os.CombinedVibrationEffect;
 import android.os.IBinder;
 import android.os.PowerManager;
@@ -388,6 +391,20 @@
     }
 
     @Test
+    public void vibrate_singleVibrator_skipsSyncedCallbacks() {
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        long vibrationId = 1;
+        waitForCompletion(startThreadAndDispatcher(vibrationId++,
+                VibrationEffect.createOneShot(10, 100)));
+
+        verify(mThreadCallbacks).onVibrationEnded(anyLong(), eq(Vibration.Status.FINISHED));
+        verify(mThreadCallbacks, never()).prepareSyncedVibration(anyLong(), any());
+        verify(mThreadCallbacks, never()).triggerSyncedVibration(anyLong());
+        verify(mThreadCallbacks, never()).cancelSyncedVibration();
+    }
+
+    @Test
     public void vibrate_multipleExistingAndMissingVibrators_vibratesOnlyExistingOnes()
             throws Exception {
         mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_TICK);
@@ -484,8 +501,7 @@
     }
 
     @Test
-    public void vibrate_multipleSequential_runsVibrationInOrderWithDelays()
-            throws Exception {
+    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);
@@ -529,6 +545,126 @@
     }
 
     @Test
+    public void vibrate_multipleSyncedCallbackTriggered_finishSteps() throws Exception {
+        int[] vibratorIds = new int[]{1, 2};
+        long vibrationId = 1;
+        mockVibrators(vibratorIds);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        when(mThreadCallbacks.prepareSyncedVibration(anyLong(), eq(vibratorIds))).thenReturn(true);
+        when(mThreadCallbacks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true);
+
+        VibrationEffect composed = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100)
+                .compose();
+        CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(composed);
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+        assertTrue(waitUntil(t -> !mVibratorProviders.get(1).getEffects().isEmpty()
+                && !mVibratorProviders.get(2).getEffects().isEmpty(), thread, TEST_TIMEOUT_MILLIS));
+        thread.syncedVibrationComplete();
+        waitForCompletion(thread);
+
+        long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_COMPOSE;
+        verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
+        verify(mThreadCallbacks).triggerSyncedVibration(eq(vibrationId));
+        verify(mThreadCallbacks, never()).cancelSyncedVibration();
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+
+        assertEquals(Arrays.asList(composed), mVibratorProviders.get(1).getEffects());
+        assertEquals(Arrays.asList(composed), mVibratorProviders.get(2).getEffects());
+    }
+
+    @Test
+    public void vibrate_multipleSynced_callsPrepareAndTriggerCallbacks() {
+        int[] vibratorIds = new int[]{1, 2, 3, 4};
+        mockVibrators(vibratorIds);
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        when(mThreadCallbacks.prepareSyncedVibration(anyLong(), any())).thenReturn(true);
+        when(mThreadCallbacks.triggerSyncedVibration(anyLong())).thenReturn(true);
+
+        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}, new int[]{100}, -1))
+                .addVibrator(4, composed)
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        long expectedCap = IVibratorManager.CAP_SYNC
+                | IVibratorManager.CAP_PREPARE_ON
+                | IVibratorManager.CAP_PREPARE_PERFORM
+                | IVibratorManager.CAP_PREPARE_COMPOSE
+                | IVibratorManager.CAP_MIXED_TRIGGER_ON
+                | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM
+                | IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
+        verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
+        verify(mThreadCallbacks).triggerSyncedVibration(eq(vibrationId));
+        verify(mThreadCallbacks, never()).cancelSyncedVibration();
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+    }
+
+    @Test
+    public void vibrate_multipleSyncedPrepareFailed_skipTriggerStepAndVibrates() {
+        int[] vibratorIds = new int[]{1, 2};
+        mockVibrators(vibratorIds);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        when(mThreadCallbacks.prepareSyncedVibration(anyLong(), any())).thenReturn(false);
+
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.createOneShot(10, 100))
+                .addVibrator(2, VibrationEffect.createWaveform(new long[]{5}, new int[]{200}, -1))
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_ON;
+        verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
+        verify(mThreadCallbacks, never()).triggerSyncedVibration(eq(vibrationId));
+        verify(mThreadCallbacks, never()).cancelSyncedVibration();
+
+        assertEquals(Arrays.asList(expectedOneShot(10)), mVibratorProviders.get(1).getEffects());
+        assertEquals(Arrays.asList(100), mVibratorProviders.get(1).getAmplitudes());
+        assertEquals(Arrays.asList(expectedOneShot(5)), mVibratorProviders.get(2).getEffects());
+        assertEquals(Arrays.asList(200), mVibratorProviders.get(2).getAmplitudes());
+    }
+
+    @Test
+    public void vibrate_multipleSyncedTriggerFailed_cancelPreparedVibrationAndSkipSetAmplitude() {
+        int[] vibratorIds = new int[]{1, 2};
+        mockVibrators(vibratorIds);
+        mVibratorProviders.get(2).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        when(mThreadCallbacks.prepareSyncedVibration(anyLong(), any())).thenReturn(true);
+        when(mThreadCallbacks.triggerSyncedVibration(anyLong())).thenReturn(false);
+
+        long vibrationId = 1;
+        CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+                .addVibrator(1, VibrationEffect.createOneShot(10, 100))
+                .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .combine();
+        VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+        waitForCompletion(thread);
+
+        long expectedCap = IVibratorManager.CAP_SYNC
+                | IVibratorManager.CAP_PREPARE_ON
+                | IVibratorManager.CAP_PREPARE_PERFORM
+                | IVibratorManager.CAP_MIXED_TRIGGER_ON
+                | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
+        verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
+        verify(mThreadCallbacks).triggerSyncedVibration(eq(vibrationId));
+        verify(mThreadCallbacks).cancelSyncedVibration();
+        assertTrue(mVibratorProviders.get(1).getAmplitudes().isEmpty());
+    }
+
+    @Test
     public void vibrate_multipleWaveforms_playsWaveformsInParallel() throws Exception {
         mockVibrators(1, 2, 3);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -623,6 +759,7 @@
 
         assertTrue(waitUntil(t -> t.getVibrators().get(2).isVibrating(), vibrationThread,
                 TEST_TIMEOUT_MILLIS));
+        assertTrue(vibrationThread.isAlive());
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.