Merge "Changes to VibrationThread in preparation for sessions" into main
diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
index b4d3862..b5a7fcb 100644
--- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
@@ -16,6 +16,7 @@
 
 package com.android.server.vibrator;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.os.ExternalVibration;
@@ -24,6 +25,7 @@
 import android.os.VibrationAttributes;
 import android.os.vibrator.Flags;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.FrameworkStatsLog;
 
 /**
@@ -32,14 +34,16 @@
 final class ExternalVibrationSession extends Vibration
         implements VibrationSession, IBinder.DeathRecipient {
 
+    private final Object mLock = new Object();
     private final ExternalVibration mExternalVibration;
     private final ExternalVibrationScale mScale = new ExternalVibrationScale();
 
+    @GuardedBy("mLock")
     @Nullable
     private Runnable mBinderDeathCallback;
 
     ExternalVibrationSession(ExternalVibration externalVibration) {
-        super(externalVibration.getToken(), new CallerInfo(
+        super(new CallerInfo(
                 externalVibration.getVibrationAttributes(), externalVibration.getUid(),
                 // TODO(b/249785241): Find a way to link ExternalVibration to a VirtualDevice
                 // instead of using DEVICE_ID_INVALID here and relying on the UID checks.
@@ -82,24 +86,25 @@
     }
 
     @Override
-    public void linkToDeath(Runnable callback) {
-        synchronized (this) {
+    public boolean linkToDeath(Runnable callback) {
+        synchronized (mLock) {
             mBinderDeathCallback = callback;
         }
         mExternalVibration.linkToDeath(this);
+        return true;
     }
 
     @Override
     public void unlinkToDeath() {
         mExternalVibration.unlinkToDeath(this);
-        synchronized (this) {
+        synchronized (mLock) {
             mBinderDeathCallback = null;
         }
     }
 
     @Override
     public void binderDied() {
-        synchronized (this) {
+        synchronized (mLock) {
             if (mBinderDeathCallback != null) {
                 mBinderDeathCallback.run();
             }
@@ -119,9 +124,11 @@
     }
 
     @Override
-    public void notifyEnded() {
+    public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy,
+            boolean immediate) {
         // Notify external client that this vibration should stop sending data to the vibrator.
         mExternalVibration.mute();
+        end(new EndInfo(status, endedBy));
     }
 
     boolean isHoldingSameVibration(ExternalVibration vib) {
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index ea4bd01..ce9c47b 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -36,6 +36,7 @@
 final class HalVibration extends Vibration {
 
     public final SparseArray<VibrationEffect> mFallbacks = new SparseArray<>();
+    public final IBinder callerToken;
 
     /** A {@link CountDownLatch} to enable waiting for completion. */
     private final CountDownLatch mCompletionLatch = new CountDownLatch(1);
@@ -55,9 +56,10 @@
     private int mScaleLevel;
     private float mAdaptiveScale;
 
-    HalVibration(@NonNull IBinder token, @NonNull CombinedVibration effect,
+    HalVibration(@NonNull IBinder callerToken, @NonNull CombinedVibration effect,
             @NonNull VibrationSession.CallerInfo callerInfo) {
-        super(token, callerInfo);
+        super(callerInfo);
+        this.callerToken = callerToken;
         mOriginalEffect = effect;
         mEffectToPlay = effect;
         mScaleLevel = VibrationScaler.SCALE_NONE;
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 9a04793..21fd4ce 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -53,16 +53,13 @@
     public final long id;
     public final VibrationSession.CallerInfo callerInfo;
     public final VibrationStats stats = new VibrationStats();
-    public final IBinder callerToken;
 
     private VibrationSession.Status mStatus;
 
-    Vibration(@NonNull IBinder token, @NonNull VibrationSession.CallerInfo callerInfo) {
-        Objects.requireNonNull(token);
+    Vibration(@NonNull VibrationSession.CallerInfo callerInfo) {
         Objects.requireNonNull(callerInfo);
         mStatus = VibrationSession.Status.RUNNING;
         this.id = sNextVibrationId.getAndIncrement();
-        this.callerToken = token;
         this.callerInfo = callerInfo;
     }
 
diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java
index 5640b49..70477a2 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSession.java
@@ -49,14 +49,26 @@
      * Links this session to the app process death with given callback to handle it.
      *
      * <p>This can be used by the service to end the vibration session when the app process dies.
+     *
+     * @param callback The service callback to be triggered when the binder dies
+     * @return true if the link was successful, false otherwise
      */
-    void linkToDeath(Runnable callback);
+    boolean linkToDeath(@Nullable Runnable callback);
 
     /** Removes link to the app process death. */
     void unlinkToDeath();
 
-    /** Notify the session end was requested, which might be acted upon asynchronously. */
-    void notifyEnded();
+    /**
+     * Notify the session end was requested, which might be acted upon asynchronously.
+     *
+     * <p>Only the first end signal will be used to end a session, but subsequent calls with
+     * {@code immediate} flag set to true can still force it to take effect urgently.
+     *
+     * @param status the end status.
+     * @param endedBy the {@link CallerInfo} of the session that requested this session to end.
+     * @param immediate indicates whether cancellation should abort urgently and skip cleanup steps.
+     */
+    void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, boolean immediate);
 
     /**
      * Session status with reference to values from vibratormanagerservice.proto for logging.
@@ -119,7 +131,7 @@
         public final String reason;
 
         CallerInfo(@NonNull VibrationAttributes attrs, int uid, int deviceId, String opPkg,
-                String reason) {
+                @Nullable String reason) {
             Objects.requireNonNull(attrs);
             this.attrs = attrs;
             this.uid = uid;
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 5137d19..1d52e3c 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,6 +21,7 @@
 import android.os.Build;
 import android.os.CombinedVibration;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.VibrationEffect;
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
@@ -38,6 +39,7 @@
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.PriorityQueue;
 import java.util.Queue;
 import java.util.concurrent.CancellationException;
@@ -358,6 +360,28 @@
     }
 
     /**
+     * Returns true if successfully linked this conductor to the death of the binder that requested
+     * the vibration.
+     */
+    public boolean linkToDeath() {
+        try {
+            mVibration.callerToken.linkToDeath(this, 0);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error linking vibration to token death", e);
+            return false;
+        }
+        return true;
+    }
+
+    public void unlinkToDeath() {
+        try {
+            mVibration.callerToken.unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            Slog.wtf(TAG, "Failed to unlink vibration to token death", e);
+        }
+    }
+
+    /**
      * Notify the execution that cancellation is requested. This will be acted upon
      * asynchronously in the VibrationThread.
      *
@@ -452,6 +476,23 @@
         }
     }
 
+    /**
+     * Notify that the VibrationThread has completed the vibration effect playback.
+     *
+     * <p>This is a lightweight method intended to be called by the vibration thread directly. The
+     * VibrationThread may still be continuing with cleanup tasks, and should not be given new work
+     * until it notifies the manager that it has been released.
+     */
+    public void notifyVibrationComplete(@NonNull Vibration.EndInfo endInfo) {
+        if (Build.IS_DEBUGGABLE) {
+            expectIsVibrationThread(true);
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "Vibration " + mVibration.id + " finished with " + endInfo);
+        }
+        mVibration.end(endInfo);
+    }
+
     /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */
     public boolean wasNotifiedToCancel() {
         if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 4c1e16c..090e679 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -18,10 +18,8 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-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.WorkSource;
@@ -31,7 +29,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.vibrator.VibrationSession.Status;
 
-import java.util.NoSuchElementException;
 import java.util.Objects;
 
 /** Plays a {@link HalVibration} in dedicated thread. */
@@ -72,14 +69,6 @@
         void noteVibratorOff(int uid);
 
         /**
-         * Tell the manager that the currently active vibration has completed its vibration, from
-         * the perspective of the Effect. However, the VibrationThread may still be continuing with
-         * cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased}
-         * is called.
-         */
-        void onVibrationCompleted(long vibrationId, @NonNull Vibration.EndInfo vibrationEndInfo);
-
-        /**
          * Tells the manager that the VibrationThread is finished with the previous vibration and
          * all of its cleanup tasks, and the vibrators can now be used for another vibration.
          */
@@ -243,7 +232,7 @@
         mWakeLock.acquire();
         try {
             try {
-                runCurrentVibrationWithWakeLockAndDeathLink();
+                playVibration();
             } finally {
                 clientVibrationCompleteIfNotAlready(
                         new Vibration.EndInfo(Status.FINISHED_UNEXPECTED));
@@ -254,41 +243,18 @@
         }
     }
 
-    /**
-     * Runs the VibrationThread with the binder death link, handling link/unlink failures.
-     * Called from within runWithWakeLock.
-     */
-    private void runCurrentVibrationWithWakeLockAndDeathLink() {
-        IBinder vibrationBinderToken = mExecutingConductor.getVibration().callerToken;
-        try {
-            vibrationBinderToken.linkToDeath(mExecutingConductor, 0);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Error linking vibration to token death", e);
-            clientVibrationCompleteIfNotAlready(
-                    new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN));
-            return;
-        }
-        // Ensure that the unlink always occurs now.
-        try {
-            // This is the actual execution of the vibration.
-            playVibration();
-        } finally {
-            try {
-                vibrationBinderToken.unlinkToDeath(mExecutingConductor, 0);
-            } catch (NoSuchElementException e) {
-                Slog.wtf(TAG, "Failed to unlink token", e);
-            }
-        }
-    }
-
     // Indicate that the vibration is complete. This can be called multiple times only for
     // convenience of handling error conditions - an error after the client is complete won't
     // affect the status.
     private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) {
         if (!mCalledVibrationCompleteCallback) {
             mCalledVibrationCompleteCallback = true;
-            mVibratorManagerHooks.onVibrationCompleted(
-                    mExecutingConductor.getVibration().id, vibrationEndInfo);
+            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
+            try {
+                mExecutingConductor.notifyVibrationComplete(vibrationEndInfo);
+            } finally {
+                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index a76d8d6..bd17e5d 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -633,12 +633,9 @@
                 final long ident = Binder.clearCallingIdentity();
                 try {
                     if (mCurrentExternalVibration != null) {
-                        mCurrentExternalVibration.notifyEnded();
                         vib.stats.reportInterruptedAnotherVibration(
-                                mCurrentExternalVibration.callerInfo);
-                        endExternalVibrateLocked(
-                                new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
-                                        vib.callerInfo),
+                                mCurrentExternalVibration.getCallerInfo());
+                        endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, vib.callerInfo,
                                 /* continueExternalControl= */ false);
                     } else if (mCurrentVibration != null) {
                         if (mCurrentVibration.getVibration().canPipelineWith(vib)) {
@@ -666,7 +663,7 @@
 
             // Ignored or failed to start the vibration, end it and report metrics right away.
             if (vibrationEndInfo != null) {
-                endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true);
+                endVibrationLocked(vib, vibrationEndInfo);
             }
             return vib;
         }
@@ -703,9 +700,8 @@
                             && shouldCancelVibration(
                             mCurrentExternalVibration.getCallerInfo().attrs,
                             usageFilter)) {
-                        mCurrentExternalVibration.notifyEnded();
-                        endExternalVibrateLocked(
-                                cancelledByUserInfo, /* continueExternalControl= */ false);
+                        endExternalVibrateLocked(cancelledByUserInfo.status,
+                                cancelledByUserInfo.endedBy, /* continueExternalControl= */ false);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -937,6 +933,11 @@
                 Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
                 // Make sure mCurrentVibration is set while triggering the VibrationThread.
                 mCurrentVibration = conductor;
+                if (!mCurrentVibration.linkToDeath()) {
+                    // Shouldn't happen. The method call already logs a wtf.
+                    mCurrentVibration = null;  // Aborted.
+                    return new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN);
+                }
                 if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) {
                     // Shouldn't happen. The method call already logs a wtf.
                     mCurrentVibration = null;  // Aborted.
@@ -953,14 +954,21 @@
     }
 
     @GuardedBy("mLock")
-    private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo,
-            boolean shouldWriteStats) {
+    private void endVibrationLocked(Vibration vib, Status status) {
+        endVibrationLocked(vib, new Vibration.EndInfo(status));
+    }
+
+    @GuardedBy("mLock")
+    private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo) {
         vib.end(vibrationEndInfo);
+        reportEndedVibrationLocked(vib);
+    }
+
+    @GuardedBy("mLock")
+    private void reportEndedVibrationLocked(Vibration vib) {
         logAndRecordVibration(vib.getDebugInfo());
-        if (shouldWriteStats) {
-            mFrameworkStatsLogger.writeVibrationReportedAsync(
-                    vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
-        }
+        mFrameworkStatsLogger.writeVibrationReportedAsync(
+                vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
     }
 
     private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
@@ -1055,17 +1063,15 @@
     }
 
     @GuardedBy("mLock")
-    private void reportFinishedVibrationLocked(Vibration.EndInfo vibrationEndInfo) {
+    private void reportFinishedVibrationLocked() {
         Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+        mCurrentVibration.unlinkToDeath();
         HalVibration vib = mCurrentVibration.getVibration();
         if (DEBUG) {
-            Slog.d(TAG, "Reporting vibration " + vib.id + " finished with "
-                    + vibrationEndInfo);
+            Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " + vib.getStatus());
         }
-        // DO NOT write metrics at this point, wait for the VibrationThread to report the
-        // vibration was released, after all cleanup. The metrics will be reported then.
-        endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false);
         finishAppOpModeLocked(vib.callerInfo);
+        reportEndedVibrationLocked(vib);
     }
 
     private void onSyncedVibrationComplete(long vibrationId) {
@@ -1647,24 +1653,6 @@
         }
 
         @Override
-        public void onVibrationCompleted(long vibrationId, Vibration.EndInfo vibrationEndInfo) {
-            if (DEBUG) {
-                Slog.d(TAG, "Vibration " + vibrationId + " finished with " + vibrationEndInfo);
-            }
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationCompleted");
-            try {
-                synchronized (mLock) {
-                    if (mCurrentVibration != null
-                            && mCurrentVibration.getVibration().id == vibrationId) {
-                        reportFinishedVibrationLocked(vibrationEndInfo);
-                    }
-                }
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-
-        @Override
         public void onVibrationThreadReleased(long vibrationId) {
             if (DEBUG) {
                 Slog.d(TAG, "VibrationThread released after finished vibration");
@@ -1682,11 +1670,8 @@
                                 mCurrentVibration.getVibration().id, vibrationId));
                     }
                     if (mCurrentVibration != null) {
-                        // This is when we consider the current vibration complete, so report
-                        // metrics.
-                        mFrameworkStatsLogger.writeVibrationReportedAsync(
-                                mCurrentVibration.getVibration().getStatsInfo(
-                                        /* completionUptimeMillis= */ SystemClock.uptimeMillis()));
+                        // This is when we consider the current vibration complete, report metrics.
+                        reportFinishedVibrationLocked();
                         mCurrentVibration = null;
                     }
                     if (mNextVibration != null) {
@@ -1696,8 +1681,7 @@
                                 nextConductor);
                         if (vibrationEndInfo != null) {
                             // Failed to start the vibration, end it and report metrics right away.
-                            endVibrationLocked(nextConductor.getVibration(),
-                                    vibrationEndInfo, /* shouldWriteStats= */ true);
+                            endVibrationLocked(nextConductor.getVibration(), vibrationEndInfo);
                         }
                     }
                 }
@@ -1929,8 +1913,7 @@
                         + " with end info: " + vibrationEndInfo);
             }
             // Clearing next vibration before playing it, end it and report metrics right away.
-            endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo,
-                    /* shouldWriteStats= */ true);
+            endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo);
             mNextVibration = null;
         }
     }
@@ -1938,23 +1921,24 @@
     /**
      * Ends the external vibration, and clears related service state.
      *
-     * @param vibrationEndInfo        the status and related info to end the associated Vibration
+     * @param status                  the status to end the associated Vibration
+     * @param endedBy                 the caller that caused this vibration to end
      * @param continueExternalControl indicates whether external control will continue. If not, the
      *                                HAL will have external control turned off.
      */
     @GuardedBy("mLock")
-    private void endExternalVibrateLocked(Vibration.EndInfo vibrationEndInfo,
+    private void endExternalVibrateLocked(Status status, CallerInfo endedBy,
             boolean continueExternalControl) {
         if (mCurrentExternalVibration == null) {
             return;
         }
+        mCurrentExternalVibration.requestEnd(status, endedBy, /* immediate= */ true);
         mCurrentExternalVibration.unlinkToDeath();
         if (!continueExternalControl) {
             setExternalControl(false, mCurrentExternalVibration.stats);
         }
         // The external control was turned off, end it and report metrics right away.
-        endVibrationLocked(mCurrentExternalVibration, vibrationEndInfo,
-                /* shouldWriteStats= */ true);
+        reportEndedVibrationLocked(mCurrentExternalVibration);
         mCurrentExternalVibration = null;
     }
 
@@ -2022,9 +2006,7 @@
 
                 synchronized (mLock) {
                     if (!hasExternalControlCapability()) {
-                        endVibrationLocked(externalVibration,
-                                new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED),
-                                /* shouldWriteStats= */ true);
+                        endVibrationLocked(externalVibration, Status.IGNORED_UNSUPPORTED);
                         return externalVibration.getScale();
                     }
 
@@ -2035,9 +2017,7 @@
                         Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
                                 + " tried to play externally controlled vibration"
                                 + " without VIBRATE permission, ignoring.");
-                        endVibrationLocked(externalVibration,
-                                new Vibration.EndInfo(Status.IGNORED_MISSING_PERMISSION),
-                                /* shouldWriteStats= */ true);
+                        endVibrationLocked(externalVibration, Status.IGNORED_MISSING_PERMISSION);
                         return externalVibration.getScale();
                     }
 
@@ -2058,8 +2038,7 @@
                     }
 
                     if (vibrationEndInfo != null) {
-                        endVibrationLocked(externalVibration, vibrationEndInfo,
-                                /* shouldWriteStats= */ true);
+                        endVibrationLocked(externalVibration, vibrationEndInfo);
                         return externalVibration.getScale();
                     }
 
@@ -2092,15 +2071,32 @@
                         // as we would need to mute the old one still if it came from a different
                         // controller.
                         alreadyUnderExternalControl = true;
-                        mCurrentExternalVibration.notifyEnded();
                         externalVibration.stats.reportInterruptedAnotherVibration(
-                                mCurrentExternalVibration.callerInfo);
-                        endExternalVibrateLocked(
-                                new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
-                                        externalVibration.callerInfo),
-                                /* continueExternalControl= */ true);
+                                mCurrentExternalVibration.getCallerInfo());
+                        endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED,
+                                externalVibration.callerInfo, /* continueExternalControl= */ true);
                     }
-
+                }
+                // Wait for lock and interact with HAL to set external control outside main lock.
+                if (waitForCompletion) {
+                    if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
+                        Slog.e(TAG, "Timed out waiting for vibration to cancel");
+                        synchronized (mLock) {
+                            endVibrationLocked(externalVibration, Status.IGNORED_ERROR_CANCELLING);
+                            return externalVibration.getScale();
+                        }
+                    }
+                }
+                if (!alreadyUnderExternalControl) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Vibrator going under external control.");
+                    }
+                    setExternalControl(true, externalVibration.stats);
+                }
+                synchronized (mLock) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Playing external vibration: " + vib);
+                    }
                     VibrationAttributes attrs = fixupVibrationAttributes(
                             vib.getVibrationAttributes(),
                             /* effect= */ null);
@@ -2113,35 +2109,12 @@
                     mCurrentExternalVibration = externalVibration;
                     externalVibration.linkToDeath(this::onExternalVibrationBinderDied);
                     externalVibration.scale(mVibrationScaler, attrs.getUsage());
-                }
 
-                if (waitForCompletion) {
-                    if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
-                        Slog.e(TAG, "Timed out waiting for vibration to cancel");
-                        synchronized (mLock) {
-                            // Trigger endExternalVibrateLocked to unlink to death recipient.
-                            endExternalVibrateLocked(
-                                    new Vibration.EndInfo(Status.IGNORED_ERROR_CANCELLING),
-                                    /* continueExternalControl= */ false);
-                            // Mute the request, vibration will be ignored.
-                            externalVibration.muteScale();
-                        }
-                        return externalVibration.getScale();
-                    }
+                    // Vibrator will start receiving data from external channels after this point.
+                    // Report current time as the vibration start time, for debugging.
+                    externalVibration.stats.reportStarted();
+                    return externalVibration.getScale();
                 }
-                if (!alreadyUnderExternalControl) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "Vibrator going under external control.");
-                    }
-                    setExternalControl(true, externalVibration.stats);
-                }
-                if (DEBUG) {
-                    Slog.d(TAG, "Playing external vibration: " + vib);
-                }
-                // Vibrator will start receiving data from external channels after this point.
-                // Report current time as the vibration start time, for debugging.
-                externalVibration.stats.reportStarted();
-                return externalVibration.getScale();
             } finally {
                 Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
             }
@@ -2157,8 +2130,7 @@
                         if (DEBUG) {
                             Slog.d(TAG, "Stopping external vibration: " + vib);
                         }
-                        endExternalVibrateLocked(
-                                new Vibration.EndInfo(Status.FINISHED),
+                        endExternalVibrateLocked(Status.FINISHED, /* endedBy= */ null,
                                 /* continueExternalControl= */ false);
                     }
                 }
@@ -2182,8 +2154,7 @@
                     if (DEBUG) {
                         Slog.d(TAG, "External vibration finished because binder died");
                     }
-                    endExternalVibrateLocked(
-                            new Vibration.EndInfo(Status.CANCELLED_BINDER_DIED),
+                    endExternalVibrateLocked(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null,
                             /* continueExternalControl= */ false);
                 }
             }
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 31cc50f1..e83a4b2 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -29,13 +29,11 @@
 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.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -96,8 +94,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 import java.util.function.BooleanSupplier;
 import java.util.stream.Collectors;
 
@@ -187,11 +183,11 @@
         mVibratorProviders.clear();
         CombinedVibration effect = CombinedVibration.createParallel(
                 VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
+        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
     }
 
     @Test
@@ -200,11 +196,11 @@
                 .addNext(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                 .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
+        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
     }
 
     @Test
@@ -212,34 +208,34 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
 
     @Test
     public void vibrate_oneShotWithoutAmplitudeControl_runsVibrationWithDefaultAmplitude() {
         VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
     }
 
@@ -249,17 +245,17 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{5, 5, 5}, new int[]{1, 2, 3}, -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(15)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 2, 3),
                 mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
@@ -277,13 +273,13 @@
         mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
         CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.completedFuture(
                 null);
-        long vibrationId = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
+        HalVibration vibration = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
                 USAGE_RINGTONE);
         waitForCompletion();
 
         verify(mStatsLoggerMock, never()).logVibrationParamRequestTimeout(UID);
         assertEquals(Arrays.asList(expectedOneShot(15)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
         for (int i = 0; i < amplitudes.size(); i++) {
             assertTrue(amplitudes.get(i) < 1 / 255f);
@@ -301,12 +297,13 @@
                 new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1);
 
         CompletableFuture<Void> neverCompletingFuture = new CompletableFuture<>();
-        long vibrationId = startThreadAndDispatcher(effect, neverCompletingFuture, USAGE_RINGTONE);
+        HalVibration vibration = startThreadAndDispatcher(effect, neverCompletingFuture,
+                USAGE_RINGTONE);
         waitForCompletion();
 
         verify(mStatsLoggerMock).logVibrationParamRequestTimeout(UID);
         assertEquals(Arrays.asList(expectedOneShot(15)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 1, 1),
                 mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
@@ -319,13 +316,13 @@
 
         int[] amplitudes = new int[]{1, 2, 3};
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5, 5, 5}, amplitudes, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(
                 waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2 * amplitudes.length,
                         TEST_TIMEOUT_MILLIS));
         // Vibration still running after 2 cycles.
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
         assertTrue(mControllers.get(VIBRATOR_ID).isVibrating());
 
         Vibration.EndInfo cancelVibrationInfo = new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
@@ -333,15 +330,15 @@
                         /* uid= */ 1, /* deviceId= */ -1, /* opPkg= */ null, /* reason= */ null));
         mVibrationConductor.notifyCancelled(cancelVibrationInfo, /* immediate= */ false);
         waitForCompletion();
-        assertFalse(mThread.isRunningVibrationId(vibrationId));
+        assertFalse(mThread.isRunningVibrationId(vibration.id));
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verifyCallbacksTriggered(vibrationId, cancelVibrationInfo);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_SUPERSEDED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         List<Float> playedAmplitudes = fakeVibrator.getAmplitudes();
-        assertFalse(fakeVibrator.getEffectSegments(vibrationId).isEmpty());
+        assertFalse(fakeVibrator.getEffectSegments(vibration.id).isEmpty());
         assertFalse(playedAmplitudes.isEmpty());
 
         for (int i = 0; i < playedAmplitudes.size(); i++) {
@@ -358,17 +355,17 @@
         int[] amplitudes = new int[]{1, 2, 3};
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{1, 10, 100}, amplitudes, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(expectedOneShot(5000)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
     }
 
     @Test
@@ -377,16 +374,16 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{0, 100, 50, 100, 0, 0, 0, 50}, /* repeat= */ -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(300L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id))
                 .isEqualTo(expectedOneShots(100L, 150L));
     }
 
@@ -398,16 +395,16 @@
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{0, 100, 0, 50, 50, 0, 100, 50}, amplitudes,
                 /* repeat= */ -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(350L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id))
                 .isEqualTo(expectedOneShots(200L, 50L));
     }
 
@@ -420,7 +417,7 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{0, 200, 50, 100, 0, 50, 50, 100}, /* repeat= */ 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         // We are expect this test to repeat the vibration effect twice, which would result in 5
         // segments being played:
         // 200ms ON
@@ -428,16 +425,17 @@
         // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
         // 150ms ON (100ms + 50ms, skips 0ms)
         // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
-        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() >= 5,
+        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() >= 5,
                 5000L + TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).subList(0, 5))
+        assertThat(
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).subList(0, 5))
                 .isEqualTo(expectedOneShots(200L, 150L, 300L, 150L, 300L));
     }
 
@@ -458,18 +456,18 @@
         VibrationEffect repeatingEffect = VibrationEffect.startComposition()
                 .repeatEffectIndefinitely(effect)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(repeatingEffect);
+        HalVibration vibration = startThreadAndDispatcher(repeatingEffect);
 
-        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
         // PWLE size max was used to generate a single vibrate call with 10 segments.
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+        assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -487,18 +485,18 @@
         VibrationEffect repeatingEffect = VibrationEffect.startComposition()
                 .repeatEffectIndefinitely(effect)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(repeatingEffect);
+        HalVibration vibration = startThreadAndDispatcher(repeatingEffect);
 
-        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false);
         waitForCompletion();
 
         // Composition size max was used to generate a single vibrate call with 10 primitives.
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+        assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -510,17 +508,17 @@
         int[] amplitudes = new int[]{1, 2, 3};
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{5000, 500, 50}, amplitudes, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(expectedOneShot(5550)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
     }
 
     @LargeTest
@@ -534,17 +532,17 @@
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{expectedOnDuration - 100, 50},
                 /* amplitudes= */ new int[]{1, 2}, /* repeat= */ 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
-        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() > 1,
+        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() > 1,
                 expectedOnDuration + TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibrationId);
+        List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibration.id);
         // First time, turn vibrator ON for the expected fixed duration.
         assertEquals(expectedOnDuration, effectSegments.get(0).getDuration());
         // Vibrator turns off in the middle of the second execution of the first step. Expect it to
@@ -567,11 +565,11 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // 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.
@@ -584,7 +582,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -597,11 +595,11 @@
         mVibratorProviders.get(VIBRATOR_ID).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS);
 
         VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // 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.
@@ -614,7 +612,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -624,11 +622,11 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
 
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{100}, new int[]{100}, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // 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.
@@ -641,7 +639,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -650,17 +648,17 @@
         mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_THUD);
 
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_THUD);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_THUD)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -671,31 +669,31 @@
         HalVibration vibration = createVibration(CombinedVibration.createParallel(
                 VibrationEffect.get(VibrationEffect.EFFECT_CLICK)));
         vibration.addFallback(VibrationEffect.EFFECT_CLICK, fallback);
-        long vibrationId = startThreadAndDispatcher(vibration);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
 
     @Test
     public void vibrate_singleVibratorPrebakedAndUnsupportedEffect_ignoresVibration() {
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L));
         verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
-        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
+        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
     }
 
     @Test
@@ -704,17 +702,17 @@
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
 
         VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID),
                 eq(PerformVendorEffectVibratorStep.VENDOR_EFFECT_MAX_DURATION_MS));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id))
                 .containsExactly(effect)
                 .inOrder();
     }
@@ -730,18 +728,18 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0),
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
     }
 
     @Test
@@ -749,14 +747,14 @@
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L));
         verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
-        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
+        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
     }
 
     @Test
@@ -774,13 +772,13 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         // Vibrator compose called twice.
-        verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        assertEquals(3, fakeVibrator.getEffectSegments(vibrationId).size());
+        verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        assertEquals(3, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -810,14 +808,14 @@
                         .build())
                 .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         // Use first duration the vibrator is turned on since we cannot estimate the clicks.
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(
                 expectedOneShot(10),
@@ -829,7 +827,7 @@
                 expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.7f,
                         /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 120, /* duration= */ 20),
                 expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
 
@@ -851,18 +849,18 @@
                 .compose();
         HalVibration vibration = createVibration(CombinedVibration.createParallel(effect));
         vibration.addFallback(VibrationEffect.EFFECT_TICK, fallback);
-        long vibrationId = startThreadAndDispatcher(vibration);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         // Use first duration the vibrator is turned on since we cannot estimate the clicks.
         verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         List<VibrationEffectSegment> segments =
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId);
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id);
         assertTrue("Wrong segments: " + segments, segments.size() >= 4);
         assertTrue(segments.get(0) instanceof PrebakedSegment);
         assertTrue(segments.get(1) instanceof PrimitiveSegment);
@@ -891,13 +889,13 @@
                 .addSustain(Duration.ofMillis(30))
                 .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
                 .build();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(
                 expectedRamp(/* amplitude= */ 1, /* frequencyHz= */ 150, /* duration= */ 10),
@@ -907,8 +905,8 @@
                 expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.6f,
                         /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200,
                         /* duration= */ 40)),
-                fakeVibrator.getEffectSegments(vibrationId));
-        assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
+        assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibration.id));
     }
 
     @Test
@@ -932,15 +930,15 @@
                 .addTransition(Duration.ofMillis(40), targetAmplitude(0.7f), targetFrequency(200))
                 .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
                 .build();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         // Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments.
         // Using best split points instead of max-packing PWLEs.
-        verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        assertEquals(6, fakeVibrator.getEffectSegments(vibrationId).size());
+        verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        assertEquals(6, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -949,28 +947,28 @@
         fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2, TEST_TIMEOUT_MILLIS));
         // Vibration still running after 2 cycles.
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
         assertTrue(mControllers.get(VIBRATOR_ID).isVibrating());
 
         mVibrationConductor.binderDied();
         waitForCompletion();
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED);
     }
 
     @Test
     public void vibrate_singleVibrator_skipsSyncedCallbacks() {
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
-        long vibrationId = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100));
+        HalVibration vibration = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100));
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         verify(mManagerHooks, never()).prepareSyncedVibration(anyLong(), any());
         verify(mManagerHooks, never()).triggerSyncedVibration(anyLong());
         verify(mManagerHooks, never()).cancelSyncedVibration();
@@ -984,18 +982,18 @@
                 .addVibrator(VIBRATOR_ID, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
                 .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_TICK)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1007,26 +1005,26 @@
 
         CombinedVibration effect = CombinedVibration.createParallel(
                 VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
 
         VibrationEffectSegment expected = expectedPrebaked(VibrationEffect.EFFECT_CLICK);
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1049,32 +1047,32 @@
                         new long[]{10, 10}, new int[]{1, 2}, -1))
                 .addVibrator(4, composed)
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).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));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(4), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
         assertFalse(mControllers.get(4).isVibrating());
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(2).getAmplitudes());
         assertEquals(Arrays.asList(expectedOneShot(20)),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 2), mVibratorProviders.get(3).getAmplitudes());
         assertEquals(Arrays.asList(
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                mVibratorProviders.get(4).getEffectSegments(vibrationId));
+                mVibratorProviders.get(4).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1094,13 +1092,13 @@
                 .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 50)
                 .addNext(2, composed, /* delay= */ 50)
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         waitForCompletion();
         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));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
 
         InOrder batteryVerifier = inOrder(mManagerHooks);
         batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
@@ -1110,19 +1108,19 @@
         batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes());
         assertEquals(Arrays.asList(
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1143,30 +1141,29 @@
         CombinedVibration effect = CombinedVibration.createParallel(composed);
         // We create the HalVibration here to obtain the vibration id and use it to mock the
         // required response when calling triggerSyncedVibration.
-        HalVibration halVibration = createVibration(effect);
-        long vibrationId = halVibration.id;
-        when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true);
-        startThreadAndDispatcher(halVibration);
+        HalVibration vibration = createVibration(effect);
+        when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true);
+        startThreadAndDispatcher(vibration);
 
         assertTrue(waitUntil(
-                () -> !mVibratorProviders.get(1).getEffectSegments(vibrationId).isEmpty()
-                        && !mVibratorProviders.get(2).getEffectSegments(vibrationId).isEmpty(),
+                () -> !mVibratorProviders.get(1).getEffectSegments(vibration.id).isEmpty()
+                        && !mVibratorProviders.get(2).getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifySyncedVibrationComplete();
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_COMPOSE;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks, never()).cancelSyncedVibration();
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         VibrationEffectSegment expected = expectedPrimitive(
                 VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100);
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1190,10 +1187,9 @@
                 .combine();
         // We create the HalVibration here to obtain the vibration id and use it to mock the
         // required response when calling triggerSyncedVibration.
-        HalVibration halVibration = createVibration(effect);
-        long vibrationId = halVibration.id;
-        when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true);
-        startThreadAndDispatcher(halVibration);
+        HalVibration vibration = createVibration(effect);
+        when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC
@@ -1204,9 +1200,9 @@
                 | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM
                 | IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks, never()).cancelSyncedVibration();
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
     }
 
     @Test
@@ -1221,19 +1217,19 @@
                 .addVibrator(1, VibrationEffect.createOneShot(10, 100))
                 .addVibrator(2, VibrationEffect.createWaveform(new long[]{5}, new int[]{200}, -1))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_ON;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks, never()).cancelSyncedVibration();
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes());
         assertEquals(Arrays.asList(expectedOneShot(5)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(200), mVibratorProviders.get(2).getAmplitudes());
     }
 
@@ -1250,10 +1246,9 @@
                 .combine();
         // We create the HalVibration here to obtain the vibration id and use it to mock the
         // required response when calling triggerSyncedVibration.
-        HalVibration halVibration = createVibration(effect);
-        long vibrationId = halVibration.id;
-        when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(false);
-        startThreadAndDispatcher(halVibration);
+        HalVibration vibration = createVibration(effect);
+        when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(false);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC
@@ -1262,7 +1257,7 @@
                 | IVibratorManager.CAP_MIXED_TRIGGER_ON
                 | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks).cancelSyncedVibration();
         assertTrue(mVibratorProviders.get(1).getAmplitudes().isEmpty());
     }
@@ -1282,7 +1277,7 @@
                 .addVibrator(3, VibrationEffect.createWaveform(
                         new long[]{60}, new int[]{6}, -1))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         // All vibrators are turned on in parallel.
         assertTrue(waitUntil(
@@ -1295,20 +1290,20 @@
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(25)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedOneShot(80)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedOneShot(60)),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 2, 3), mVibratorProviders.get(1).getAmplitudes());
         assertEquals(expectedAmplitudes(4, 5), mVibratorProviders.get(2).getAmplitudes());
         assertEquals(expectedAmplitudes(6), mVibratorProviders.get(3).getAmplitudes());
@@ -1327,17 +1322,11 @@
                 CombinedVibration.createParallel(
                         VibrationEffect.createOneShot(
                                 expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
-        CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
-        doAnswer(unused -> {
-            vibrationCompleteLatch.countDown();
-            return null;
-        }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
 
         startThreadAndDispatcher(vibration);
         long startTime = SystemClock.elapsedRealtime();
 
-        assertTrue(vibrationCompleteLatch.await(expectedDuration + TEST_TIMEOUT_MILLIS,
-                TimeUnit.MILLISECONDS));
+        vibration.waitForEnd();
         long vibrationEndTime = SystemClock.elapsedRealtime();
 
         waitForCompletion(rampDownDuration + TEST_TIMEOUT_MILLIS);
@@ -1363,17 +1352,11 @@
                 CombinedVibration.createParallel(
                         VibrationEffect.createOneShot(
                                 expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
-        CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
-        doAnswer(unused -> {
-            vibrationCompleteLatch.countDown();
-            return null;
-        }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
 
         startThreadAndDispatcher(vibration);
         long startTime = SystemClock.elapsedRealtime();
 
-        assertTrue(vibrationCompleteLatch.await(callbackDelay + TEST_TIMEOUT_MILLIS,
-                TimeUnit.MILLISECONDS));
+        vibration.waitForEnd();
         long vibrationEndTime = SystemClock.elapsedRealtime();
 
         waitForCompletion(TEST_TIMEOUT_MILLIS);
@@ -1397,17 +1380,11 @@
                 CombinedVibration.createParallel(
                         VibrationEffect.createOneShot(
                                 expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
-        CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
-        doAnswer(unused -> {
-            vibrationCompleteLatch.countDown();
-            return null;
-        }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
 
         startThreadAndDispatcher(vibration);
         long startTime = SystemClock.elapsedRealtime();
 
-        assertTrue(vibrationCompleteLatch.await(callbackTimeout + TEST_TIMEOUT_MILLIS,
-                TimeUnit.MILLISECONDS));
+        vibration.waitForEnd();
         long vibrationEndTime = SystemClock.elapsedRealtime();
 
         waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS);
@@ -1461,11 +1438,11 @@
         fakeVibrator.setOnLatency(latency);
 
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
-        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(cancellingThread).
@@ -1480,7 +1457,7 @@
 
         // After the vibrator call ends the vibration is cancelled and the vibrator is turned off.
         waitForCompletion(/* timeout= */ latency + TEST_TIMEOUT_MILLIS);
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -1500,10 +1477,10 @@
                         .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
                         .compose())
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // 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.
@@ -1516,7 +1493,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
     }
@@ -1534,10 +1511,10 @@
                 .addVibrator(1, VibrationEffect.createVendorEffect(createTestVendorData()))
                 .addVibrator(2, VibrationEffect.createVendorEffect(createTestVendorData()))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // 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.
@@ -1550,7 +1527,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
     }
@@ -1566,12 +1543,12 @@
                         new long[]{100, 100}, new int[]{1, 2}, 0))
                 .addVibrator(2, VibrationEffect.createOneShot(100, 100))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(1).isVibrating()
                         && mControllers.get(2).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // 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.
@@ -1584,7 +1561,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
     }
@@ -1592,19 +1569,17 @@
     @Test
     public void vibrate_binderDied_cancelsVibration() throws Exception {
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         mVibrationConductor.binderDied();
         waitForCompletion();
 
-        verify(mVibrationToken).linkToDeath(same(mVibrationConductor), eq(0));
-        verify(mVibrationToken).unlinkToDeath(same(mVibrationConductor), eq(0));
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED);
-        assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED);
+        assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -1615,15 +1590,15 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{5, 5, 5}, new int[]{60, 120, 240}, -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         // Duration extended for 5 + 5 + 5 + 15.
         assertEquals(Arrays.asList(expectedOneShot(30)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
         assertTrue(amplitudes.size() > 3);
         assertEquals(expectedAmplitudes(60, 120, 240), amplitudes.subList(0, 3));
@@ -1633,24 +1608,24 @@
     }
 
     @Test
-    public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds() {
+    public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds()
+            throws Exception {
         when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(10_000);
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createOneShot(10, 200);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         // Vibration completed but vibrator not yet released.
-        verify(mManagerHooks, timeout(TEST_TIMEOUT_MILLIS)).onVibrationCompleted(eq(vibrationId),
-                eq(new Vibration.EndInfo(Status.FINISHED)));
+        vibration.waitForEnd();
         verify(mManagerHooks, never()).onVibrationThreadReleased(anyLong());
 
         // Thread still running ramp down.
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Duration extended for 10 + 10000.
         assertEquals(Arrays.asList(expectedOneShot(10_010)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
 
         // Will stop the ramp down right away.
         mVibrationConductor.notifyCancelled(
@@ -1658,9 +1633,8 @@
         waitForCompletion();
 
         // Does not cancel already finished vibration, but releases vibrator.
-        verify(mManagerHooks, never()).onVibrationCompleted(eq(vibrationId),
-                eq(new Vibration.EndInfo(Status.CANCELLED_BY_SETTINGS_UPDATE)));
-        verify(mManagerHooks).onVibrationThreadReleased(vibrationId);
+        assertThat(vibration.getStatus()).isNotEqualTo(Status.CANCELLED_BY_SETTINGS_UPDATE);
+        verify(mManagerHooks).onVibrationThreadReleased(vibration.id);
     }
 
     @Test
@@ -1670,18 +1644,18 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createOneShot(10_000, 240);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
 
         // Duration extended for 10000 + 15.
         assertEquals(Arrays.asList(expectedOneShot(10_015)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
         assertTrue(amplitudes.size() > 1);
         for (int i = 1; i < amplitudes.size(); i++) {
@@ -1696,14 +1670,14 @@
         mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
 
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
     }
 
@@ -1714,13 +1688,13 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
 
         VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id))
                 .containsExactly(effect)
                 .inOrder();
         assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty();
@@ -1737,15 +1711,15 @@
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         assertEquals(
                 Arrays.asList(expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
     }
 
@@ -1764,14 +1738,14 @@
         VibrationEffect effect = VibrationEffect.startWaveform()
                 .addTransition(Duration.ofMillis(1), targetAmplitude(1))
                 .build();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         assertEquals(Arrays.asList(expectedRamp(0, 1, 150, 150, 1)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
         assertTrue(fakeVibrator.getAmplitudes().isEmpty());
     }
 
@@ -1796,69 +1770,68 @@
         VibrationEffect effect4 = VibrationEffect.createOneShot(8000, 100);
         VibrationEffect effect5 = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
 
-        long vibrationId1 = startThreadAndDispatcher(effect1);
+        HalVibration vibration1 = startThreadAndDispatcher(effect1);
         waitForCompletion();
-        verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1);
-        verifyCallbacksTriggered(vibrationId1, Status.FINISHED);
 
-        long vibrationId2 = startThreadAndDispatcher(effect2);
+        HalVibration vibration2 = startThreadAndDispatcher(effect2);
         // Effect2 won't complete on its own. Cancel it after a couple of repeats.
         Thread.sleep(150);  // More than two TICKs.
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        long vibrationId3 = startThreadAndDispatcher(effect3);
+        HalVibration vibration3 = startThreadAndDispatcher(effect3);
         waitForCompletion();
 
         // Effect4 is a long oneshot, but it gets cancelled as fast as possible.
         long start4 = System.currentTimeMillis();
-        long vibrationId4 = startThreadAndDispatcher(effect4);
+        HalVibration vibration4 = startThreadAndDispatcher(effect4);
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ true);
         waitForCompletion();
         long duration4 = System.currentTimeMillis() - start4;
 
         // Effect5 is to show that things keep going after the immediate cancel.
-        long vibrationId5 = startThreadAndDispatcher(effect5);
+        HalVibration vibration5 = startThreadAndDispatcher(effect5);
         waitForCompletion();
 
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         // Effect1
-        verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1);
-        verifyCallbacksTriggered(vibrationId1, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibration1.id);
+        verifyCallbacksTriggered(vibration1, Status.FINISHED);
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                fakeVibrator.getEffectSegments(vibrationId1));
+                fakeVibrator.getEffectSegments(vibration1.id));
 
         // Effect2: repeating, cancelled.
-        verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibrationId2);
-        verifyCallbacksTriggered(vibrationId2, Status.CANCELLED_BY_USER);
+        verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibration2.id);
+        verifyCallbacksTriggered(vibration2, Status.CANCELLED_BY_USER);
 
         // The exact count of segments might vary, so just check that there's more than 2 and
         // all elements are the same segment.
-        List<VibrationEffectSegment> actualSegments2 = fakeVibrator.getEffectSegments(vibrationId2);
+        List<VibrationEffectSegment> actualSegments2 =
+                fakeVibrator.getEffectSegments(vibration2.id);
         assertTrue(actualSegments2.size() + " > 2", actualSegments2.size() > 2);
         for (VibrationEffectSegment segment : actualSegments2) {
             assertEquals(expectedPrebaked(VibrationEffect.EFFECT_TICK), segment);
         }
 
         // Effect3
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId3));
-        verifyCallbacksTriggered(vibrationId3, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration3.id));
+        verifyCallbacksTriggered(vibration3, Status.FINISHED);
         assertEquals(Arrays.asList(
                         expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                fakeVibrator.getEffectSegments(vibrationId3));
+                fakeVibrator.getEffectSegments(vibration3.id));
 
         // Effect4: cancelled quickly.
-        verifyCallbacksTriggered(vibrationId4, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration4, Status.CANCELLED_BY_SCREEN_OFF);
         assertTrue("Tested duration=" + duration4, duration4 < 2000);
 
         // Effect5: played normally after effect4, which may or may not have played.
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                fakeVibrator.getEffectSegments(vibrationId5));
+                fakeVibrator.getEffectSegments(vibration5.id));
     }
 
     private void mockVibrators(int... vibratorIds) {
@@ -1875,19 +1848,19 @@
         mVibrationSettings.mSettingObserver.onChange(false);
     }
 
-    private long startThreadAndDispatcher(VibrationEffect effect) {
+    private HalVibration startThreadAndDispatcher(VibrationEffect effect) {
         return startThreadAndDispatcher(CombinedVibration.createParallel(effect));
     }
 
-    private long startThreadAndDispatcher(CombinedVibration effect) {
+    private HalVibration startThreadAndDispatcher(CombinedVibration effect) {
         return startThreadAndDispatcher(createVibration(effect));
     }
 
-    private long startThreadAndDispatcher(HalVibration vib) {
+    private HalVibration startThreadAndDispatcher(HalVibration vib) {
         return startThreadAndDispatcher(vib, /* requestVibrationParamsFuture= */ null);
     }
 
-    private long startThreadAndDispatcher(VibrationEffect effect,
+    private HalVibration startThreadAndDispatcher(VibrationEffect effect,
             CompletableFuture<Void> requestVibrationParamsFuture, int usage) {
         VibrationAttributes attrs = new VibrationAttributes.Builder()
                 .setUsage(usage)
@@ -1898,14 +1871,14 @@
         return startThreadAndDispatcher(vib, requestVibrationParamsFuture);
     }
 
-    private long startThreadAndDispatcher(HalVibration vib,
+    private HalVibration startThreadAndDispatcher(HalVibration vib,
             CompletableFuture<Void> requestVibrationParamsFuture) {
         mControllers = createVibratorControllers();
         DeviceAdapter deviceAdapter = new DeviceAdapter(mVibrationSettings, mControllers);
         mVibrationConductor = new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter,
                 mVibrationScaler, mStatsLoggerMock, requestVibrationParamsFuture, mManagerHooks);
         assertTrue(mThread.runVibrationOnVibrationThread(mVibrationConductor));
-        return mVibrationConductor.getVibration().id;
+        return mVibrationConductor.getVibration();
     }
 
     private boolean waitUntil(BooleanSupplier predicate, long timeout)
@@ -1994,13 +1967,9 @@
                 .collect(Collectors.toList());
     }
 
-    private void verifyCallbacksTriggered(long vibrationId, Status expectedStatus) {
-        verifyCallbacksTriggered(vibrationId, new Vibration.EndInfo(expectedStatus));
-    }
-
-    private void verifyCallbacksTriggered(long vibrationId, Vibration.EndInfo expectedEndInfo) {
-        verify(mManagerHooks).onVibrationCompleted(eq(vibrationId), eq(expectedEndInfo));
-        verify(mManagerHooks).onVibrationThreadReleased(vibrationId);
+    private void verifyCallbacksTriggered(HalVibration vibration, Status expectedStatus) {
+        assertThat(vibration.getStatus()).isEqualTo(expectedStatus);
+        verify(mManagerHooks).onVibrationThreadReleased(vibration.id);
     }
 
     private static final class TestLooperAutoDispatcher extends Thread {