Introduce VendorVibrationSession

Introduce Vibrator APIs to start vendor vibration sessions.

Vendor sessions will play vibration requests without resetting the
vibrator hardware state, allowing the vendor to customize the behaviour
between vibrations.

Vibration sessions can be ended by the client app or by the vibrator
service, allowing higher priority vibrations (e.g. ringtone, alarms) to
take control of the vibrator and play as usual.

Bug: 345414356
Test: FrameworksVibratorServicesTests
Flag: android.os.vibrator.vendor_vibration_effects
Change-Id: Id749cc240201bf398526c2416d5767a364b86cbf
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index e4d5589..264599a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -388,6 +388,7 @@
     field public static final String START_CROSS_PROFILE_ACTIVITIES = "android.permission.START_CROSS_PROFILE_ACTIVITIES";
     field public static final String START_REVIEW_PERMISSION_DECISIONS = "android.permission.START_REVIEW_PERMISSION_DECISIONS";
     field public static final String START_TASKS_FROM_RECENTS = "android.permission.START_TASKS_FROM_RECENTS";
+    field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String START_VIBRATION_SESSIONS = "android.permission.START_VIBRATION_SESSIONS";
     field public static final String STATUS_BAR_SERVICE = "android.permission.STATUS_BAR_SERVICE";
     field public static final String STOP_APP_SWITCHES = "android.permission.STOP_APP_SWITCHES";
     field public static final String SUBSTITUTE_NOTIFICATION_APP_NAME = "android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME";
@@ -11642,8 +11643,11 @@
   public abstract class Vibrator {
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener);
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.Vibrator.OnVibratorStateChangedListener);
+    method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorEffectsSupported();
+    method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorSessionsSupported();
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public boolean isVibrating();
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void removeVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener);
+    method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") @RequiresPermission(allOf={android.Manifest.permission.VIBRATE, android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, android.Manifest.permission.START_VIBRATION_SESSIONS}) public void startVendorSession(@NonNull android.os.VibrationAttributes, @Nullable String, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.vibrator.VendorVibrationSession.Callback);
   }
 
   public static interface Vibrator.OnVibratorStateChangedListener {
@@ -11795,6 +11799,28 @@
 
 }
 
+package android.os.vibrator {
+
+  @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public final class VendorVibrationSession implements java.lang.AutoCloseable {
+    method public void cancel();
+    method public void close();
+    method @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(@NonNull android.os.VibrationEffect, @Nullable String);
+    field public static final int STATUS_CANCELED = 4; // 0x4
+    field public static final int STATUS_IGNORED = 2; // 0x2
+    field public static final int STATUS_SUCCESS = 1; // 0x1
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+    field public static final int STATUS_UNKNOWN_ERROR = 5; // 0x5
+    field public static final int STATUS_UNSUPPORTED = 3; // 0x3
+  }
+
+  public static interface VendorVibrationSession.Callback {
+    method public void onFinished(int);
+    method public void onFinishing();
+    method public void onStarted(@NonNull android.os.vibrator.VendorVibrationSession);
+  }
+
+}
+
 package android.os.vibrator.persistence {
 
   @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl
index 6aa9852..ecb5e6f 100644
--- a/core/java/android/os/IVibratorManagerService.aidl
+++ b/core/java/android/os/IVibratorManagerService.aidl
@@ -17,13 +17,17 @@
 package android.os;
 
 import android.os.CombinedVibration;
+import android.os.ICancellationSignal;
 import android.os.IVibratorStateListener;
 import android.os.VibrationAttributes;
 import android.os.VibratorInfo;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
 
 /** {@hide} */
 interface IVibratorManagerService {
     int[] getVibratorIds();
+    int getCapabilities();
     VibratorInfo getVibratorInfo(int vibratorId);
     @EnforcePermission("ACCESS_VIBRATOR_STATE")
     boolean isVibrating(int vibratorId);
@@ -50,4 +54,9 @@
     oneway void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg,
             int constant, int inputDeviceId, int inputSource, String reason, int flags,
             int privFlags);
+
+    @EnforcePermission(allOf={"VIBRATE", "VIBRATE_VENDOR_EFFECTS", "START_VIBRATION_SESSIONS"})
+    ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg,
+            in int[] vibratorIds, in VibrationAttributes attributes, String reason,
+            in IVibrationSessionCallback callback);
 }
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 011a3ee..c3cddf3 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -18,8 +18,11 @@
 
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.hardware.vibrator.IVibratorManager;
+import android.os.vibrator.VendorVibrationSession;
 import android.os.vibrator.VibratorInfoFactory;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -53,6 +56,7 @@
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private VibratorInfo mVibratorInfo;
+    private int[] mVibratorIds;
 
     @UnsupportedAppUsage
     public SystemVibrator(Context context) {
@@ -71,7 +75,11 @@
                 Log.w(TAG, "Failed to retrieve vibrator info; no vibrator manager.");
                 return VibratorInfo.EMPTY_VIBRATOR_INFO;
             }
-            int[] vibratorIds = mVibratorManager.getVibratorIds();
+            int[] vibratorIds = getVibratorIds();
+            if (vibratorIds == null) {
+                Log.w(TAG, "Failed to retrieve vibrator info; error retrieving vibrator ids.");
+                return VibratorInfo.EMPTY_VIBRATOR_INFO;
+            }
             if (vibratorIds.length == 0) {
                 // It is known that the device has no vibrator, so cache and return info that
                 // reflects the lack of support for effects/primitives.
@@ -95,20 +103,22 @@
 
     @Override
     public boolean hasVibrator() {
-        if (mVibratorManager == null) {
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
             Log.w(TAG, "Failed to check if vibrator exists; no vibrator manager.");
             return false;
         }
-        return mVibratorManager.getVibratorIds().length > 0;
+        return vibratorIds.length > 0;
     }
 
     @Override
     public boolean isVibrating() {
-        if (mVibratorManager == null) {
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
             Log.w(TAG, "Failed to vibrate; no vibrator manager.");
             return false;
         }
-        for (int vibratorId : mVibratorManager.getVibratorIds()) {
+        for (int vibratorId : vibratorIds) {
             if (mVibratorManager.getVibrator(vibratorId).isVibrating()) {
                 return true;
             }
@@ -136,6 +146,11 @@
             Log.w(TAG, "Failed to add vibrate state listener; no vibrator manager.");
             return;
         }
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
+            Log.w(TAG, "Failed to add vibrate state listener; error retrieving vibrator ids.");
+            return;
+        }
         MultiVibratorStateListener delegate = null;
         try {
             synchronized (mRegisteredListeners) {
@@ -145,7 +160,7 @@
                     return;
                 }
                 delegate = new MultiVibratorStateListener(executor, listener);
-                delegate.register(mVibratorManager);
+                delegate.register(mVibratorManager, vibratorIds);
                 mRegisteredListeners.put(listener, delegate);
                 delegate = null;
             }
@@ -184,6 +199,11 @@
     }
 
     @Override
+    public boolean areVendorSessionsSupported() {
+        return mVibratorManager.hasCapabilities(IVibratorManager.CAP_START_SESSIONS);
+    }
+
+    @Override
     public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId, VibrationEffect effect,
             VibrationAttributes attrs) {
         if (mVibratorManager == null) {
@@ -243,6 +263,41 @@
         mVibratorManager.cancel(usageFilter);
     }
 
+    @Override
+    public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason,
+            @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor,
+            @NonNull VendorVibrationSession.Callback callback) {
+        if (mVibratorManager == null) {
+            Log.w(TAG, "Failed to start vibration session; no vibrator manager.");
+            executor.execute(
+                    () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR));
+            return;
+        }
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
+            Log.w(TAG, "Failed to start vibration session; error retrieving vibrator ids.");
+            executor.execute(
+                    () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR));
+            return;
+        }
+        mVibratorManager.startVendorSession(vibratorIds, attrs, reason, cancellationSignal,
+                executor, callback);
+    }
+
+    @Nullable
+    private int[] getVibratorIds() {
+        synchronized (mLock) {
+            if (mVibratorIds != null) {
+                return mVibratorIds;
+            }
+            if (mVibratorManager == null) {
+                Log.w(TAG, "Failed to retrieve vibrator ids; no vibrator manager.");
+                return null;
+            }
+            return mVibratorIds = mVibratorManager.getVibratorIds();
+        }
+    }
+
     /**
      * Tries to unregister individual {@link android.os.Vibrator.OnVibratorStateChangedListener}
      * that were left registered to vibrators after failures to register them to all vibrators.
@@ -319,8 +374,7 @@
         }
 
         /** Registers a listener to all individual vibrators in {@link VibratorManager}. */
-        public void register(VibratorManager vibratorManager) {
-            int[] vibratorIds = vibratorManager.getVibratorIds();
+        public void register(VibratorManager vibratorManager, @NonNull int[] vibratorIds) {
             synchronized (mLock) {
                 for (int i = 0; i < vibratorIds.length; i++) {
                     int vibratorId = vibratorIds[i];
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index a5697fb..f9935d2 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -22,6 +22,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.vibrator.IVibratorManager;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
+import android.os.vibrator.VendorVibrationSession;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
@@ -47,6 +51,8 @@
     @GuardedBy("mLock")
     private int[] mVibratorIds;
     @GuardedBy("mLock")
+    private int mCapabilities;
+    @GuardedBy("mLock")
     private final SparseArray<Vibrator> mVibrators = new SparseArray<>();
 
     @GuardedBy("mLock")
@@ -84,6 +90,11 @@
         }
     }
 
+    @Override
+    public boolean hasCapabilities(int capabilities) {
+        return (getCapabilities() & capabilities) == capabilities;
+    }
+
     @NonNull
     @Override
     public Vibrator getVibrator(int vibratorId) {
@@ -173,7 +184,7 @@
             int inputSource, String reason, int flags, int privFlags) {
         if (mService == null) {
             Log.w(TAG, "Failed to perform haptic feedback for input device;"
-                            + " no vibrator manager service.");
+                    + " no vibrator manager service.");
             return;
         }
         Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
@@ -197,6 +208,50 @@
         cancelVibration(usageFilter);
     }
 
+    @Override
+    public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs,
+            @Nullable String reason, @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) {
+        Objects.requireNonNull(vibratorIds);
+        VendorVibrationSessionCallbackDelegate callbackDelegate =
+                new VendorVibrationSessionCallbackDelegate(executor, callback);
+        if (mService == null) {
+            Log.w(TAG, "Failed to start vibration session; no vibrator manager service.");
+            callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR);
+            return;
+        }
+        try {
+            ICancellationSignal remoteCancellationSignal = mService.startVendorVibrationSession(
+                    mUid, mContext.getDeviceId(), mPackageName, vibratorIds, attrs, reason,
+                    callbackDelegate);
+            if (cancellationSignal != null) {
+                cancellationSignal.setRemote(remoteCancellationSignal);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to start vibration session.", e);
+            callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR);
+        }
+    }
+
+    private int getCapabilities() {
+        synchronized (mLock) {
+            if (mCapabilities != 0) {
+                return mCapabilities;
+            }
+            try {
+                if (mService == null) {
+                    Log.w(TAG, "Failed to retrieve vibrator manager capabilities;"
+                            + " no vibrator manager service.");
+                } else {
+                    return mCapabilities = mService.getCapabilities();
+                }
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            return 0;
+        }
+    }
+
     private void cancelVibration(int usageFilter) {
         if (mService == null) {
             Log.w(TAG, "Failed to cancel vibration; no vibrator manager service.");
@@ -228,12 +283,45 @@
         }
     }
 
+    /** Callback for vendor vibration sessions. */
+    private static class VendorVibrationSessionCallbackDelegate extends
+            IVibrationSessionCallback.Stub {
+        private final Executor mExecutor;
+        private final VendorVibrationSession.Callback mCallback;
+
+        VendorVibrationSessionCallbackDelegate(
+                @NonNull Executor executor,
+                @NonNull VendorVibrationSession.Callback callback) {
+            Objects.requireNonNull(executor);
+            Objects.requireNonNull(callback);
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onStarted(IVibrationSession session) {
+            mExecutor.execute(() -> mCallback.onStarted(new VendorVibrationSession(session)));
+        }
+
+        @Override
+        public void onFinishing() {
+            mExecutor.execute(() -> mCallback.onFinishing());
+        }
+
+        @Override
+        public void onFinished(int status) {
+            mExecutor.execute(() -> mCallback.onFinished(status));
+        }
+    }
+
     /** Controls vibrations on a single vibrator. */
     private final class SingleVibrator extends Vibrator {
         private final VibratorInfo mVibratorInfo;
+        private final int[] mVibratorId;
 
         SingleVibrator(@NonNull VibratorInfo vibratorInfo) {
             mVibratorInfo = vibratorInfo;
+            mVibratorId = new int[]{mVibratorInfo.getId()};
         }
 
         @Override
@@ -252,6 +340,11 @@
         }
 
         @Override
+        public boolean areVendorSessionsSupported() {
+            return SystemVibratorManager.this.hasCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        }
+
+        @Override
         public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
                 @Nullable VibrationEffect effect, @Nullable VibrationAttributes attrs) {
             CombinedVibration combined = CombinedVibration.startParallel()
@@ -369,5 +462,13 @@
                 }
             }
         }
+
+        @Override
+        public void startVendorSession(@NonNull VibrationAttributes attrs, String reason,
+                @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor,
+                @NonNull VendorVibrationSession.Callback callback) {
+            SystemVibratorManager.this.startVendorSession(mVibratorId, attrs, reason,
+                    cancellationSignal, executor, callback);
+        }
     }
 }
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index c4c4580..53f8a92 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -33,6 +33,7 @@
 import android.hardware.vibrator.IVibrator;
 import android.media.AudioAttributes;
 import android.os.vibrator.Flags;
+import android.os.vibrator.VendorVibrationSession;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibratorFrequencyProfile;
 import android.os.vibrator.VibratorFrequencyProfileLegacy;
@@ -247,6 +248,34 @@
     }
 
     /**
+     * Check whether the vibrator has support for vendor-specific effects.
+     *
+     * <p>Vendor vibration effects can be created via {@link VibrationEffect#createVendorEffect}.
+     *
+     * @return True if the hardware can play vendor-specific vibration effects, false otherwise.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public boolean areVendorEffectsSupported() {
+        return getInfo().hasCapability(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+    }
+
+    /**
+     * Check whether the vibrator has support for vendor-specific vibration sessions.
+     *
+     * <p>Vendor vibration sessions can be started via {@link #startVendorSession}.
+     *
+     * @return True if the hardware can play vendor-specific vibration sessions, false otherwise.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public boolean areVendorSessionsSupported() {
+        return false;
+    }
+
+    /**
      * Check whether the vibrator can be controlled by an external service with the
      * {@link IExternalVibratorService}.
      *
@@ -922,4 +951,44 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE)
     public void removeVibratorStateListener(@NonNull OnVibratorStateChangedListener listener) {
     }
+
+    /**
+     * Starts a vibration session in this vibrator.
+     *
+     * <p>The session will start asynchronously once the vibrator control can be acquired. Once it's
+     * started the {@link VendorVibrationSession} will be provided to the callback. This session
+     * should be used to play vibrations until the session is ended or canceled.
+     *
+     * <p>The vendor app will have exclusive control over the vibrator during this session. This
+     * control can be revoked by the vibrator service, which will be notified to the same session
+     * callback with the {@link VendorVibrationSession#STATUS_CANCELED}.
+     *
+     * <p>The {@link VibrationAttributes} will be used to decide the priority of the vendor
+     * vibrations that will be performed in this session. All vibrations within this session will
+     * apply the same attributes.
+     *
+     * @param attrs    The {@link VibrationAttributes} corresponding to the vibrations that will be
+     *                 performed in the session. This will be used to decide the priority of this
+     *                 session against other system vibrations.
+     * @param reason   The description for this session, used for debugging purposes.
+     * @param cancellationSignal A signal to cancel the session before it starts.
+     * @param executor The executor for the session callbacks.
+     * @param callback The {@link VendorVibrationSession.Callback} for the started session.
+     *
+     * @see VendorVibrationSession
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.VIBRATE,
+            android.Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+            android.Manifest.permission.START_VIBRATION_SESSIONS,
+    })
+    public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason,
+            @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor,
+            @NonNull VendorVibrationSession.Callback callback) {
+        Log.w(TAG, "startVendorSession is not supported");
+        executor.execute(() -> callback.onFinished(VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
 }
diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java
index 0428876..0072bc2 100644
--- a/core/java/android/os/VibratorManager.java
+++ b/core/java/android/os/VibratorManager.java
@@ -22,9 +22,12 @@
 import android.annotation.SystemService;
 import android.app.ActivityThread;
 import android.content.Context;
+import android.os.vibrator.VendorVibrationSession;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
 
+import java.util.concurrent.Executor;
+
 /**
  * Provides access to all vibrators from the device, as well as the ability to run them
  * in a synchronized fashion.
@@ -62,6 +65,14 @@
     public abstract int[] getVibratorIds();
 
     /**
+     * Return true if the vibrator manager has all capabilities, false otherwise.
+     * @hide
+     */
+    public boolean hasCapabilities(int capabilities) {
+        return false;
+    }
+
+    /**
      * Retrieve a single vibrator by id.
      *
      * @param vibratorId The id of the vibrator to be retrieved.
@@ -190,4 +201,30 @@
      */
     @RequiresPermission(android.Manifest.permission.VIBRATE)
     public abstract void cancel(int usageFilter);
+
+
+    /**
+     * Starts a vibration session on given vibrators.
+     *
+     * @param vibratorIds The vibrators that will be controlled by this session.
+     * @param attrs       The {@link VibrationAttributes} corresponding to the vibrations that will
+     *                    be performed in the session. This will be used to decide the priority of
+     *                    this session against other system vibrations.
+     * @param reason      The description for this session, used for debugging purposes.
+     * @param cancellationSignal A signal to cancel the session before it starts.
+     * @param executor    The executor for the session callbacks.
+     * @param callback    The {@link VendorVibrationSession.Callback} for the started session.
+     * @see Vibrator#startVendorSession
+     * @hide
+     */
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.VIBRATE,
+            android.Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+            android.Manifest.permission.START_VIBRATION_SESSIONS,
+    })
+    public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs,
+            @Nullable String reason, @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) {
+        Log.w(TAG, "startVendorSession is not supported");
+    }
 }
diff --git a/core/java/android/os/vibrator/IVibrationSession.aidl b/core/java/android/os/vibrator/IVibrationSession.aidl
new file mode 100644
index 0000000..e829549
--- /dev/null
+++ b/core/java/android/os/vibrator/IVibrationSession.aidl
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.os.vibrator;
+
+import android.os.CombinedVibration;
+
+/**
+ * The communication channel by which an app control the system vibrators.
+ *
+ * In order to synchronize the places where vibrations might be controlled we provide this interface
+ * so the vibrator subsystem has a chance to:
+ *
+ * 1) Decide whether the current session should have the vibrator control.
+ * 2) Stop any on-going session for a new session/vibration, based on current system policy.
+ * {@hide}
+ */
+interface IVibrationSession {
+    const int STATUS_UNKNOWN = 0;
+    const int STATUS_SUCCESS = 1;
+    const int STATUS_IGNORED = 2;
+    const int STATUS_UNSUPPORTED = 3;
+    const int STATUS_CANCELED = 4;
+    const int STATUS_UNKNOWN_ERROR = 5;
+
+    /**
+     * A method called to start a vibration within this session. This will fail if the session
+     * is finishing or was canceled.
+     */
+    void vibrate(in CombinedVibration vibration, String reason);
+
+    /**
+     * A method called by the app to stop this session gracefully. The vibrator will complete any
+     * ongoing vibration before the session is ended.
+     */
+    void finishSession();
+
+    /**
+     * A method called by the app to stop this session immediatelly by interrupting any ongoing
+     * vibration.
+     */
+    void cancelSession();
+}
diff --git a/core/java/android/os/vibrator/IVibrationSessionCallback.aidl b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl
new file mode 100644
index 0000000..36c3695
--- /dev/null
+++ b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.os.vibrator;
+
+import android.os.vibrator.IVibrationSession;
+
+/**
+ * Callback for vibration session state.
+ * {@hide}
+ */
+oneway interface IVibrationSessionCallback {
+
+    /**
+     * A method called by the service after a vibration session has successfully started. After this
+     * is called the app has control over the vibrator through this given session.
+     */
+    void onStarted(in IVibrationSession session);
+
+    /**
+     * A method called by the service to indicate the session is ending and should no longer receive
+     * vibration requests.
+     */
+    void onFinishing();
+
+    /**
+     * A method called by the service after the session has ended. This might be triggered by the
+     * app or the service. The status code indicates the end reason.
+     */
+    void onFinished(int status);
+}
diff --git a/core/java/android/os/vibrator/VendorVibrationSession.java b/core/java/android/os/vibrator/VendorVibrationSession.java
new file mode 100644
index 0000000..c23f2ed
--- /dev/null
+++ b/core/java/android/os/vibrator/VendorVibrationSession.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.vibrator;
+
+import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.CombinedVibration;
+import android.os.RemoteException;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A vendor session that temporarily gains control over the system vibrators.
+ *
+ * <p>Vibration effects can be played by the vibrator in a vendor session via {@link #vibrate}. The
+ * effects will be forwarded to the vibrator hardware immediately. Any concurrency support is
+ * defined and controlled by the vibrator hardware implementation.
+ *
+ * <p>The session should be ended by {@link #close()}, which will wait until the last vibration ends
+ * and the vibrator is released. The end of the session will be notified to the {@link Callback}
+ * provided when the session was created.
+ *
+ * <p>Any ongoing session can be immediately interrupted by the vendor app via {@link #cancel()},
+ * including after {@link #close()} was called and the session is tearing down. A session can also
+ * be canceled by the vibrator service when it needs to regain control of the system vibrators.
+ *
+ * @see Vibrator#startVendorSession
+ * @hide
+ */
+@FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS)
+@SystemApi
+public final class VendorVibrationSession implements AutoCloseable {
+    private static final String TAG = "VendorVibrationSession";
+
+    /**
+     * The session ended successfully.
+     */
+    public static final int STATUS_SUCCESS = IVibrationSession.STATUS_SUCCESS;
+
+    /**
+     * The session was ignored.
+     *
+     * <p>This might be caused by user settings, vibration policies or the device state that
+     * prevents the app from performing vibrations for the requested
+     * {@link android.os.VibrationAttributes}.
+     */
+    public static final int STATUS_IGNORED = IVibrationSession.STATUS_IGNORED;
+
+    /**
+     * The session is not supported.
+     *
+     * <p>The support for vendor vibration sessions can be checked via
+     * {@link Vibrator#areVendorSessionsSupported()}.
+     */
+    public static final int STATUS_UNSUPPORTED = IVibrationSession.STATUS_UNSUPPORTED;
+
+    /**
+     * The session was canceled.
+     *
+     * <p>This might be triggered by the app after a session starts via {@link #cancel()}, or it
+     * can be triggered by the platform before or after the session has started.
+     */
+    public static final int STATUS_CANCELED = IVibrationSession.STATUS_CANCELED;
+
+    /**
+     * The session status is unknown.
+     */
+    public static final int STATUS_UNKNOWN = IVibrationSession.STATUS_UNKNOWN;
+
+    /**
+     * The session failed with unknown error.
+     *
+     * <p>This can be caused by a failure to start a vibration session or after it has started, to
+     * indicate it has ended unexpectedly because of a system failure.
+     */
+    public static final int STATUS_UNKNOWN_ERROR = IVibrationSession.STATUS_UNKNOWN_ERROR;
+
+    /** @hide */
+    @IntDef(prefix = { "STATUS_" }, value = {
+            STATUS_SUCCESS,
+            STATUS_IGNORED,
+            STATUS_UNSUPPORTED,
+            STATUS_CANCELED,
+            STATUS_UNKNOWN,
+            STATUS_UNKNOWN_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Status{}
+
+    private final IVibrationSession mSession;
+
+    /** @hide */
+    public VendorVibrationSession(@NonNull IVibrationSession session) {
+        Objects.requireNonNull(session);
+        mSession = session;
+    }
+
+    /**
+     * Vibrate with a given effect.
+     *
+     * <p>The vibration will be sent to the vibrator hardware immediately, without waiting for any
+     * previous vibration completion. The vendor should control the concurrency behavior at the
+     * hardware level (e.g. queueing, mixing, interrupting).
+     *
+     * <p>If the provided effect is played by the vibrator service with controlled timings (e.g.
+     * effects created via {@link VibrationEffect#createWaveform}), then triggering a new vibration
+     * will cause the ongoing playback to be interrupted in favor of the new vibration. If the
+     * effect is broken down into multiple consecutive commands (e.g. large primitive compositions)
+     * then the hardware commands will be triggered in succession without waiting for the completion
+     * callback.
+     *
+     * <p>The vendor app is responsible for timing the session requests and the vibrator hardware
+     * implementation is free to handle concurrency with different policies.
+     *
+     * @param effect The {@link VibrationEffect} describing the vibration to be performed.
+     * @param reason The description for the vibration reason, for debugging purposes.
+     */
+    @RequiresPermission(android.Manifest.permission.VIBRATE)
+    public void vibrate(@NonNull VibrationEffect effect, @Nullable String reason) {
+        try {
+            mSession.vibrate(CombinedVibration.createParallel(effect), reason);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to vibrate in a vendor vibration session.", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Cancel ongoing session.
+     *
+     * <p>This will stop the vibration immediately and return the vibrator control to the
+     * platform. This can also be triggered after {@link #close()} to immediately release the
+     * vibrator.
+     *
+     * <p>This will trigger {@link VendorVibrationSession.Callback#onFinished} directly with
+     * {@link #STATUS_CANCELED}.
+     */
+    public void cancel() {
+        try {
+            mSession.cancelSession();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to cancel vendor vibration session.", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * End ongoing session gracefully.
+     *
+     * <p>This might continue the vibration while it's ramping down and wrapping up the session
+     * in the vibrator hardware. No more vibration commands can be sent through this session
+     * after this method is called.
+     *
+     * <p>This will trigger {@link VendorVibrationSession.Callback#onFinishing()}.
+     */
+    @Override
+    public void close() {
+        try {
+            mSession.finishSession();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to finish vendor vibration session.", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Callbacks for {@link VendorVibrationSession} events.
+     *
+     * @see Vibrator#startVendorSession
+     * @see VendorVibrationSession
+     */
+    public interface Callback {
+
+        /**
+         * New session was successfully started.
+         *
+         * <p>The vendor app can interact with the vibrator using the
+         * {@link VendorVibrationSession} provided.
+         */
+        void onStarted(@NonNull VendorVibrationSession session);
+
+        /**
+         * The session is ending and finishing any pending vibrations.
+         *
+         * <p>This is only invoked after {@link #onStarted(VendorVibrationSession)}. It will be
+         * triggered by both {@link VendorVibrationSession#cancel()} and
+         * {@link VendorVibrationSession#close()}. This might also be triggered if the platform
+         * cancels the ongoing session.
+         *
+         * <p>Session vibrations might be still ongoing in the vibrator hardware but the app can
+         * no longer send commands through the session. A finishing session can still be immediately
+         * stopped via calls to {@link VendorVibrationSession.Callback#cancel()}.
+         */
+        void onFinishing();
+
+        /**
+         * The session is finished.
+         *
+         * <p>The vibrator has finished any vibration and returned to the platform's control. This
+         * might be triggered by the vendor app or by the vibrator service.
+         *
+         * <p>If this is triggered before {@link #onStarted} then the session was finished before
+         * starting, either because it was cancelled or failed to start. If the session has already
+         * started then this will be triggered after {@link #onFinishing()} to indicate all session
+         * vibrations are complete and the vibrator is no longer under the session's control.
+         *
+         * @param status The session status.
+         */
+        void onFinished(@VendorVibrationSession.Status int status);
+    }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5044a30..d1dd1a6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2622,13 +2622,22 @@
 
     <!-- @SystemApi Allows access to perform vendor effects in the vibrator.
          <p>Protection level: signature
-         @FlaggedApi("android.os.vibrator.vendor_vibration_effects")
+         @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
          @hide
     -->
     <permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS"
         android:protectionLevel="signature|privileged"
         android:featureFlag="android.os.vibrator.vendor_vibration_effects" />
 
+    <!-- @SystemApi Allows access to start a vendor vibration session.
+         <p>Protection level: signature
+        @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+        @hide
+    -->
+    <permission android:name="android.permission.START_VIBRATION_SESSIONS"
+        android:protectionLevel="signature|privileged"
+        android:featureFlag="android.os.vibrator.vendor_vibration_effects" />
+
     <!-- @SystemApi Allows access to the vibrator state.
          <p>Protection level: signature
          @hide
diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java
index 6210a00..09bfadb 100644
--- a/core/tests/vibrator/src/android/os/VibratorTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorTest.java
@@ -110,8 +110,9 @@
 
     @Test
     public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() {
+        int[] vibratorIds = new int[0];
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]);
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
 
         Vibrator.OnVibratorStateChangedListener mockListener =
                 mock(Vibrator.OnVibratorStateChangedListener.class);
@@ -119,7 +120,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
 
         // Never tries to register a listener to an individual vibrator.
         assertFalse(multiVibratorListener.hasRegisteredListeners());
@@ -128,8 +129,9 @@
 
     @Test
     public void onVibratorStateChanged_singleVibrator_forwardsAllCallbacks() {
+        int[] vibratorIds = new int[] { 1 };
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1 });
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
         when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
 
         Vibrator.OnVibratorStateChangedListener mockListener =
@@ -138,7 +140,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
         assertTrue(multiVibratorListener.hasRegisteredListeners());
 
         multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false);
@@ -156,8 +158,9 @@
 
     @Test
     public void onVibratorStateChanged_multipleVibrators_triggersOnlyWhenAllVibratorsInitialized() {
+        int[] vibratorIds = new int[] { 1, 2 };
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 });
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
         when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
 
         Vibrator.OnVibratorStateChangedListener mockListener =
@@ -166,7 +169,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
         assertTrue(multiVibratorListener.hasRegisteredListeners());
 
         multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false);
@@ -181,8 +184,9 @@
 
     @Test
     public void onVibratorStateChanged_multipleVibrators_stateChangeIsDeduped() {
+        int[] vibratorIds = new int[] { 1, 2 };
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 });
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
         when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
 
         Vibrator.OnVibratorStateChangedListener mockListener =
@@ -191,7 +195,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
         assertTrue(multiVibratorListener.hasRegisteredListeners());
 
         multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); // none
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 56e55df..7ced809 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -402,6 +402,9 @@
         <permission name="android.permission.SHOW_CUSTOMIZED_RESOLVER"/>
         <!-- Permission required for access VIBRATOR_STATE. -->
         <permission name="android.permission.ACCESS_VIBRATOR_STATE"/>
+        <!-- Permission required for vendor vibration effects and sessions. -->
+        <permission name="android.permission.VIBRATE_VENDOR_EFFECTS"/>
+        <permission name="android.permission.START_VIBRATION_SESSIONS"/>
         <!-- Permission required for UsageStatsTest CTS test. -->
         <permission name="android.permission.MANAGE_NOTIFICATIONS"/>
         <!-- Permission required for CompanionDeviceManager CTS test. -->
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 1919572..aa847ac 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -263,6 +263,8 @@
     <uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE" />
+    <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" />
+    <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" />
     <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
     <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" />
     <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" />
diff --git a/services/art-profile b/services/art-profile
index 6fa4c88..ce1e2c6 100644
--- a/services/art-profile
+++ b/services/art-profile
@@ -5657,7 +5657,7 @@
 Lcom/android/server/utils/Watcher;
 Lcom/android/server/vibrator/VibratorController$NativeWrapper;
 Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener;
-Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;
+Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;
 Lcom/android/server/vibrator/VibratorManagerService;
 Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener;
 Lcom/android/server/vr/VrManagerService;
diff --git a/services/art-wear-profile b/services/art-wear-profile
index 47bdb13..1e3090f 100644
--- a/services/art-wear-profile
+++ b/services/art-wear-profile
@@ -1330,7 +1330,7 @@
 Lcom/android/server/utils/Watcher;
 Lcom/android/server/vibrator/VibratorController$NativeWrapper;
 Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener;
-Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;
+Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;
 Lcom/android/server/vibrator/VibratorManagerService;
 Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener;
 Lcom/android/server/vr/VrManagerService;
@@ -24948,7 +24948,7 @@
 PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->cancelSynced()V
 PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getCapabilities()J
 PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getVibratorIds()[I
-PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)V
+PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)V
 PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;-><init>(Lcom/android/server/vibrator/VibratorManagerService;)V
 PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;->onComplete(IJ)V
 PLcom/android/server/vibrator/VibratorManagerService$VibrationRecords;-><init>(II)V
diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
index df44e50..a92ac67 100644
--- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
@@ -45,6 +45,7 @@
         void onExternalVibrationReleased(long vibrationId);
     }
 
+    private final long mSessionId = VibrationSession.nextSessionId();
     private final ExternalVibration mExternalVibration;
     private final ExternalVibrationScale mScale = new ExternalVibrationScale();
     private final VibratorManagerHooks mManagerHooks;
@@ -65,6 +66,11 @@
     }
 
     @Override
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    @Override
     public long getCreateUptimeMillis() {
         return stats.getCreateUptimeMillis();
     }
@@ -148,7 +154,12 @@
 
     @Override
     public void notifySyncedVibratorsCallback(long vibrationId) {
-        // ignored, external control does not expect callbacks from the vibrator manager
+        // ignored, external control does not expect callbacks from the vibrator manager for sync
+    }
+
+    @Override
+    public void notifySessionCallback() {
+        // ignored, external control does not expect callbacks from the vibrator manager for session
     }
 
     boolean isHoldingSameVibration(ExternalVibration vib) {
@@ -174,7 +185,8 @@
     @Override
     public String toString() {
         return "ExternalVibrationSession{"
-                + "id=" + id
+                + "sessionId=" + mSessionId
+                + ", vibrationId=" + id
                 + ", callerInfo=" + callerInfo
                 + ", externalVibration=" + mExternalVibration
                 + ", scale=" + mScale
diff --git a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
index 67ba25f..628221b 100644
--- a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
@@ -35,6 +35,7 @@
     private static final String TAG = "SingleVibrationSession";
 
     private final Object mLock = new Object();
+    private final long mSessionId = VibrationSession.nextSessionId();
     private final IBinder mCallerToken;
     private final HalVibration mVibration;
 
@@ -58,6 +59,11 @@
     }
 
     @Override
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    @Override
     public long getCreateUptimeMillis() {
         return mVibration.stats.getCreateUptimeMillis();
     }
@@ -155,9 +161,15 @@
     }
 
     @Override
+    public void notifySessionCallback() {
+        // ignored, external control does not expect callbacks from the vibrator manager for session
+    }
+
+    @Override
     public String toString() {
         return "SingleVibrationSession{"
-                + "callerToken= " + mCallerToken
+                + "sessionId= " + mSessionId
+                + ", callerToken= " + mCallerToken
                 + ", vibration=" + mVibration
                 + '}';
     }
diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
new file mode 100644
index 0000000..07478e3
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioAttributes;
+import android.os.CancellationSignal;
+import android.os.CombinedVibration;
+import android.os.ExternalVibration;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.VibrationAttributes;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+/**
+ * A vibration session started by a vendor request that can trigger {@link CombinedVibration}.
+ */
+final class VendorVibrationSession extends IVibrationSession.Stub
+        implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient {
+    private static final String TAG = "VendorVibrationSession";
+
+    /** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */
+    interface VibratorManagerHooks {
+
+        /** Tells the manager to end the vibration session. */
+        void endSession(long sessionId, boolean shouldAbort);
+
+        /**
+         * Tells the manager that the vibration session is finished and the vibrators can now be
+         * used for another vibration.
+         */
+        void onSessionReleased(long sessionId);
+    }
+
+    private final Object mLock = new Object();
+    private final long mSessionId = VibrationSession.nextSessionId();
+    private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport();
+    private final int[] mVibratorIds;
+    private final long mCreateUptime;
+    private final long mCreateTime; // for debugging
+    private final IVibrationSessionCallback mCallback;
+    private final CallerInfo mCallerInfo;
+    private final VibratorManagerHooks mManagerHooks;
+    private final Handler mHandler;
+
+    @GuardedBy("mLock")
+    private Status mStatus = Status.RUNNING;
+    @GuardedBy("mLock")
+    private Status mEndStatusRequest;
+    @GuardedBy("mLock")
+    private long mStartTime; // for debugging
+    @GuardedBy("mLock")
+    private long mEndUptime;
+    @GuardedBy("mLock")
+    private long mEndTime; // for debugging
+
+    VendorVibrationSession(@NonNull CallerInfo callerInfo, @NonNull Handler handler,
+            @NonNull VibratorManagerHooks managerHooks, @NonNull int[] vibratorIds,
+            @NonNull IVibrationSessionCallback callback) {
+        mCreateUptime = SystemClock.uptimeMillis();
+        mCreateTime = System.currentTimeMillis();
+        mVibratorIds = vibratorIds;
+        mHandler = handler;
+        mCallback = callback;
+        mCallerInfo = callerInfo;
+        mManagerHooks = managerHooks;
+        CancellationSignal.fromTransport(mCancellationSignal).setOnCancelListener(this);
+    }
+
+    @Override
+    public void vibrate(CombinedVibration vibration, String reason) {
+        // TODO(b/345414356): implement vibration support
+        throw new UnsupportedOperationException("Vendor session vibrations not yet implemented");
+    }
+
+    @Override
+    public void finishSession() {
+        // Do not abort session in HAL, wait for ongoing vibration requests to complete.
+        // This might take a while to end the session, but it can be aborted by cancelSession.
+        requestEndSession(Status.FINISHED, /* shouldAbort= */ false);
+    }
+
+    @Override
+    public void cancelSession() {
+        // Always abort session in HAL while cancelling it.
+        // This might be triggered after finishSession was already called.
+        requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true);
+    }
+
+    @Override
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    @Override
+    public long getCreateUptimeMillis() {
+        return mCreateUptime;
+    }
+
+    @Override
+    public boolean isRepeating() {
+        return false;
+    }
+
+    @Override
+    public CallerInfo getCallerInfo() {
+        return mCallerInfo;
+    }
+
+    @Override
+    public IBinder getCallerToken() {
+        return mCallback.asBinder();
+    }
+
+    @Override
+    public DebugInfo getDebugInfo() {
+        synchronized (mLock) {
+            return new DebugInfoImpl(mStatus, mCallerInfo, mCreateUptime, mCreateTime, mStartTime,
+                    mEndUptime, mEndTime);
+        }
+    }
+
+    @Override
+    public boolean wasEndRequested() {
+        synchronized (mLock) {
+            return mEndStatusRequest != null;
+        }
+    }
+
+    @Override
+    public void onCancel() {
+        Slog.d(TAG, "Cancellation signal received, cancelling vibration session...");
+        requestEnd(Status.CANCELLED_BY_USER, /* endedBy= */ null, /* immediate= */ false);
+    }
+
+    @Override
+    public void binderDied() {
+        Slog.d(TAG, "Binder died, cancelling vibration session...");
+        requestEnd(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null, /* immediate= */ false);
+    }
+
+    @Override
+    public boolean linkToDeath() {
+        try {
+            mCallback.asBinder().linkToDeath(this, 0);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error linking session to token death", e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void unlinkToDeath() {
+        try {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            Slog.wtf(TAG, "Failed to unlink session to token death", e);
+        }
+    }
+
+    @Override
+    public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy,
+            boolean immediate) {
+        // All requests to end a session should abort it to stop ongoing vibrations, even if
+        // immediate flag is false. Only the #finishSession API will not abort and wait for
+        // session vibrations to complete, which might take a long time.
+        requestEndSession(status, /* shouldAbort= */ true);
+    }
+
+    @Override
+    public void notifyVibratorCallback(int vibratorId, long vibrationId) {
+        // TODO(b/345414356): implement vibration support
+    }
+
+    @Override
+    public void notifySyncedVibratorsCallback(long vibrationId) {
+        // TODO(b/345414356): implement vibration support
+    }
+
+    @Override
+    public void notifySessionCallback() {
+        synchronized (mLock) {
+            // If end was not requested then the HAL has cancelled the session.
+            maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON);
+            maybeSetStatusToRequestedLocked();
+        }
+        mManagerHooks.onSessionReleased(mSessionId);
+    }
+
+    @Override
+    public String toString() {
+        synchronized (mLock) {
+            return "createTime: " + formatTime(mCreateTime, /*includeDate=*/ true)
+                    + ", startTime: " + (mStartTime == 0 ? null : formatTime(mStartTime,
+                    /* includeDate= */ true))
+                    + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime,
+                    /* includeDate= */ true))
+                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
+                    + ", callerInfo: " + mCallerInfo
+                    + ", vibratorIds: " + Arrays.toString(mVibratorIds);
+        }
+    }
+
+    public Status getStatus() {
+        synchronized (mLock) {
+            return mStatus;
+        }
+    }
+
+    public boolean isStarted() {
+        synchronized (mLock) {
+            return mStartTime > 0;
+        }
+    }
+
+    public boolean isEnded() {
+        synchronized (mLock) {
+            return mStatus != Status.RUNNING;
+        }
+    }
+
+    public int[] getVibratorIds() {
+        return mVibratorIds;
+    }
+
+    public ICancellationSignal getCancellationSignal() {
+        return mCancellationSignal;
+    }
+
+    public void notifyStart() {
+        boolean isAlreadyEnded = false;
+        synchronized (mLock) {
+            if (isEnded()) {
+                // Session already ended, skip start callbacks.
+                isAlreadyEnded = true;
+            } else {
+                mStartTime = System.currentTimeMillis();
+                // Run client callback in separate thread.
+                mHandler.post(() -> {
+                    try {
+                        mCallback.onStarted(this);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error notifying vendor session started", e);
+                    }
+                });
+            }
+        }
+        if (isAlreadyEnded) {
+            // Session already ended, make sure we end it in the HAL.
+            mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true);
+        }
+    }
+
+    private void requestEndSession(Status status, boolean shouldAbort) {
+        boolean shouldTriggerSessionHook = false;
+        synchronized (mLock) {
+            maybeSetEndRequestLocked(status);
+            if (isStarted()) {
+                // Always trigger session hook after it has started, in case new request aborts an
+                // already finishing session. Wait for HAL callback before actually ending here.
+                shouldTriggerSessionHook = true;
+            } else {
+                // Session did not start in the HAL, end it right away.
+                maybeSetStatusToRequestedLocked();
+            }
+        }
+        if (shouldTriggerSessionHook) {
+            mManagerHooks.endSession(mSessionId, shouldAbort);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void maybeSetEndRequestLocked(Status status) {
+        if (mEndStatusRequest != null) {
+            // End already requested, keep first requested status and time.
+            return;
+        }
+        mEndStatusRequest = status;
+        mEndTime = System.currentTimeMillis();
+        mEndUptime = SystemClock.uptimeMillis();
+        if (isStarted()) {
+            // Only trigger "finishing" callback if session started.
+            // Run client callback in separate thread.
+            mHandler.post(() -> {
+                try {
+                    mCallback.onFinishing();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error notifying vendor session is finishing", e);
+                }
+            });
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void maybeSetStatusToRequestedLocked() {
+        if (isEnded()) {
+            // End already set, keep first requested status and time.
+            return;
+        }
+        if (mEndStatusRequest == null) {
+            // No end status was requested, nothing to set.
+            return;
+        }
+        mStatus = mEndStatusRequest;
+        // Run client callback in separate thread.
+        final Status endStatus = mStatus;
+        mHandler.post(() -> {
+            try {
+                mCallback.onFinished(toSessionStatus(endStatus));
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error notifying vendor session is finishing", e);
+            }
+        });
+    }
+
+    @android.os.vibrator.VendorVibrationSession.Status
+    private static int toSessionStatus(Status status) {
+        // Exhaustive switch to cover all possible internal status.
+        return switch (status) {
+            case FINISHED
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
+            case IGNORED_UNSUPPORTED
+                    -> STATUS_UNSUPPORTED;
+            case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
+                 CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
+                 CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
+            case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
+                 IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
+                 IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
+                 IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
+            case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING,
+                 IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
+        };
+    }
+
+    /**
+     * Holds lightweight debug information about the session that could potentially be kept in
+     * memory for a long time for bugreport dumpsys operations.
+     *
+     * Since DebugInfo can be kept in memory for a long time, it shouldn't hold any references to
+     * potentially expensive or resource-linked objects, such as {@link IBinder}.
+     */
+    static final class DebugInfoImpl implements VibrationSession.DebugInfo {
+        private final Status mStatus;
+        private final CallerInfo mCallerInfo;
+
+        private final long mCreateUptime;
+        private final long mCreateTime;
+        private final long mStartTime;
+        private final long mEndTime;
+        private final long mDurationMs;
+
+        DebugInfoImpl(Status status, CallerInfo callerInfo, long createUptime, long createTime,
+                long startTime, long endUptime, long endTime) {
+            mStatus = status;
+            mCallerInfo = callerInfo;
+            mCreateUptime = createUptime;
+            mCreateTime = createTime;
+            mStartTime = startTime;
+            mEndTime = endTime;
+            mDurationMs = endUptime > 0 ? endUptime - createUptime : -1;
+        }
+
+        @Override
+        public Status getStatus() {
+            return mStatus;
+        }
+
+        @Override
+        public long getCreateUptimeMillis() {
+            return mCreateUptime;
+        }
+
+        @Override
+        public CallerInfo getCallerInfo() {
+            return mCallerInfo;
+        }
+
+        @Nullable
+        @Override
+        public Object getDumpAggregationKey() {
+            return null; // No aggregation.
+        }
+
+        @Override
+        public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
+        }
+
+        @Override
+        public void dump(ProtoOutputStream proto, long fieldId) {
+            final long token = proto.start(fieldId);
+            proto.write(VibrationProto.END_TIME, mEndTime);
+            proto.write(VibrationProto.DURATION_MS, mDurationMs);
+            proto.write(VibrationProto.STATUS, mStatus.ordinal());
+
+            final long attrsToken = proto.start(VibrationProto.ATTRIBUTES);
+            final VibrationAttributes attrs = mCallerInfo.attrs;
+            proto.write(VibrationAttributesProto.USAGE, attrs.getUsage());
+            proto.write(VibrationAttributesProto.AUDIO_USAGE, attrs.getAudioUsage());
+            proto.write(VibrationAttributesProto.FLAGS, attrs.getFlags());
+            proto.end(attrsToken);
+
+            proto.end(token);
+        }
+
+        @Override
+        public void dump(IndentingPrintWriter pw) {
+            pw.println("VibrationSession:");
+            pw.increaseIndent();
+            pw.println("status = " + mStatus.name().toLowerCase(Locale.ROOT));
+            pw.println("durationMs = " + mDurationMs);
+            pw.println("createTime = " + formatTime(mCreateTime, /*includeDate=*/ true));
+            pw.println("startTime = " + formatTime(mStartTime, /*includeDate=*/ true));
+            pw.println("endTime = " + (mEndTime == 0 ? null
+                    : formatTime(mEndTime, /*includeDate=*/ true)));
+            pw.println("callerInfo = " + mCallerInfo);
+            pw.decreaseIndent();
+        }
+
+        @Override
+        public void dumpCompact(IndentingPrintWriter pw) {
+            // Follow pattern from Vibration.DebugInfoImpl for better debugging from dumpsys.
+            String timingsStr = String.format(Locale.ROOT,
+                    "%s | %8s | %20s | duration: %5dms | start: %12s | end: %12s",
+                    formatTime(mCreateTime, /*includeDate=*/ true),
+                    "session",
+                    mStatus.name().toLowerCase(Locale.ROOT),
+                    mDurationMs,
+                    mStartTime == 0 ? "" : formatTime(mStartTime, /*includeDate=*/ false),
+                    mEndTime == 0 ? "" : formatTime(mEndTime, /*includeDate=*/ false));
+            String paramStr = String.format(Locale.ROOT,
+                    " | flags: %4s | usage: %s",
+                    Long.toBinaryString(mCallerInfo.attrs.getFlags()),
+                    mCallerInfo.attrs.usageToString());
+            // Optional, most vibrations should not be defined via AudioAttributes
+            // so skip them to simplify the logs
+            String audioUsageStr =
+                    mCallerInfo.attrs.getOriginalAudioUsage() != AudioAttributes.USAGE_UNKNOWN
+                            ? " | audioUsage=" + AudioAttributes.usageToString(
+                            mCallerInfo.attrs.getOriginalAudioUsage())
+                            : "";
+            String callerStr = String.format(Locale.ROOT,
+                    " | %s (uid=%d, deviceId=%d) | reason: %s",
+                    mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.deviceId, mCallerInfo.reason);
+            pw.println(timingsStr + paramStr + audioUsageStr + callerStr);
+        }
+
+        @Override
+        public String toString() {
+            return "createTime: " + formatTime(mCreateTime, /* includeDate= */ true)
+                    + ", startTime: " + formatTime(mStartTime, /* includeDate= */ true)
+                    + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime,
+                    /* includeDate= */ true))
+                    + ", durationMs: " + mDurationMs
+                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
+                    + ", callerInfo: " + mCallerInfo;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index bb2a17c..27f92b2 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -16,6 +16,8 @@
 
 package com.android.server.vibrator;
 
+import static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.media.AudioAttributes;
@@ -31,9 +33,6 @@
 import android.util.IndentingPrintWriter;
 import android.util.proto.ProtoOutputStream;
 
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -42,11 +41,6 @@
  * The base class for all vibrations.
  */
 abstract class Vibration {
-    private static final DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern(
-            "HH:mm:ss.SSS");
-    private static final DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
-            "MM-dd HH:mm:ss.SSS");
-
     // Used to generate globally unique vibration ids.
     private static final AtomicInteger sNextVibrationId = new AtomicInteger(1); // 0 = no callback
 
@@ -399,12 +393,5 @@
             proto.write(PrimitiveSegmentProto.DELAY, segment.getDelay());
             proto.end(token);
         }
-
-        private String formatTime(long timeInMillis, boolean includeDate) {
-            return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER)
-                    // Ensure timezone is retrieved at formatting time
-                    .withZone(ZoneId.systemDefault())
-                    .format(Instant.ofEpochMilli(timeInMillis));
-        }
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java
index b511ba8..ae95a70 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSession.java
@@ -25,7 +25,11 @@
 import android.util.proto.ProtoOutputStream;
 
 import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Represents a generic vibration session that plays one or more vibration requests.
@@ -39,6 +43,16 @@
  */
 interface VibrationSession {
 
+    // Used to generate globally unique session ids.
+    AtomicInteger sNextSessionId = new AtomicInteger(1); // 0 = no callback
+
+    static long nextSessionId() {
+        return sNextSessionId.getAndIncrement();
+    }
+
+    /** Returns the session id. */
+    long getSessionId();
+
     /** Returns the session creation time from {@link android.os.SystemClock#uptimeMillis()}. */
     long getCreateUptimeMillis();
 
@@ -105,6 +119,14 @@
     void notifySyncedVibratorsCallback(long vibrationId);
 
     /**
+     * Notify vibrator manager have completed the vibration session.
+     *
+     * <p>This will be called by the vibrator manager hardware callback indicating the session
+     * is complete, either because it was ended or cancelled by the service or the vendor.
+     */
+    void notifySessionCallback();
+
+    /**
      * Session status with reference to values from vibratormanagerservice.proto for logging.
      */
     enum Status {
@@ -212,6 +234,17 @@
      */
     interface DebugInfo {
 
+        DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
+        DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
+                "MM-dd HH:mm:ss.SSS");
+
+        static String formatTime(long timeInMillis, boolean includeDate) {
+            return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER)
+                    // Ensure timezone is retrieved at formatting time
+                    .withZone(ZoneId.systemDefault())
+                    .format(Instant.ofEpochMilli(timeInMillis));
+        }
+
         /** Return the vibration session status. */
         Status getStatus();
 
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index ff34911..4764481 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -32,6 +32,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.hardware.vibrator.IVibrator;
+import android.hardware.vibrator.IVibratorManager;
 import android.os.BatteryStats;
 import android.os.Binder;
 import android.os.Build;
@@ -40,6 +41,7 @@
 import android.os.ExternalVibrationScale;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.ICancellationSignal;
 import android.os.IExternalVibratorService;
 import android.os.IVibratorManagerService;
 import android.os.IVibratorStateListener;
@@ -57,6 +59,7 @@
 import android.os.VibrationEffect;
 import android.os.VibratorInfo;
 import android.os.vibrator.Flags;
+import android.os.vibrator.IVibrationSessionCallback;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
@@ -103,7 +106,7 @@
     private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service";
     private static final String VIBRATOR_CONTROL_SERVICE =
             "android.frameworks.vibrator.IVibratorControlService/default";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
     private static final VibrationAttributes DEFAULT_ATTRIBUTES =
             new VibrationAttributes.Builder().build();
     private static final int ATTRIBUTES_ALL_BYPASS_FLAGS =
@@ -159,12 +162,14 @@
             new VibrationThreadCallbacks();
     private final ExternalVibrationCallbacks mExternalVibrationCallbacks =
             new ExternalVibrationCallbacks();
+    private final VendorVibrationSessionCallbacks mVendorVibrationSessionCallbacks =
+            new VendorVibrationSessionCallbacks();
     @GuardedBy("mLock")
     private final SparseArray<AlwaysOnVibration> mAlwaysOnEffects = new SparseArray<>();
     @GuardedBy("mLock")
-    private VibrationSession mCurrentVibration;
+    private VibrationSession mCurrentSession;
     @GuardedBy("mLock")
-    private VibrationSession mNextVibration;
+    private VibrationSession mNextSession;
     @GuardedBy("mLock")
     private boolean mServiceReady;
 
@@ -191,14 +196,14 @@
                 // When the system is entering a non-interactive state, we want to cancel
                 // vibrations in case a misbehaving app has abandoned them.
                 synchronized (mLock) {
-                    maybeClearCurrentAndNextVibrationsLocked(
+                    maybeClearCurrentAndNextSessionsLocked(
                             VibratorManagerService.this::shouldCancelOnScreenOffLocked,
                             Status.CANCELLED_BY_SCREEN_OFF);
                 }
             } else if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers()
                     && intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) {
                 synchronized (mLock) {
-                    maybeClearCurrentAndNextVibrationsLocked(
+                    maybeClearCurrentAndNextSessionsLocked(
                             VibratorManagerService.this::shouldCancelOnFgUserRequest,
                             Status.CANCELLED_BY_FOREGROUND_USER);
                 }
@@ -215,14 +220,14 @@
                         return;
                     }
                     synchronized (mLock) {
-                        maybeClearCurrentAndNextVibrationsLocked(
+                        maybeClearCurrentAndNextSessionsLocked(
                                 VibratorManagerService.this::shouldCancelAppOpModeChangedLocked,
                                 Status.CANCELLED_BY_APP_OPS);
                     }
                 }
             };
 
-    static native long nativeInit(OnSyncedVibrationCompleteListener listener);
+    static native long nativeInit(VibratorManagerNativeCallbacks listener);
 
     static native long nativeGetFinalizer();
 
@@ -236,6 +241,13 @@
 
     static native void nativeCancelSynced(long nativeServicePtr);
 
+    static native boolean nativeStartSession(long nativeServicePtr, long sessionId,
+            int[] vibratorIds);
+
+    static native void nativeEndSession(long nativeServicePtr, long sessionId, boolean shouldAbort);
+
+    static native void nativeClearSessions(long nativeServicePtr);
+
     @VisibleForTesting
     VibratorManagerService(Context context, Injector injector) {
         mContext = context;
@@ -303,6 +315,9 @@
         // Reset the hardware to a default state, in case this is a runtime restart instead of a
         // fresh boot.
         mNativeWrapper.cancelSynced();
+        if (Flags.vendorVibrationEffects()) {
+            mNativeWrapper.clearSessions();
+        }
         for (int i = 0; i < mVibrators.size(); i++) {
             mVibrators.valueAt(i).reset();
         }
@@ -363,6 +378,11 @@
     }
 
     @Override // Binder call
+    public int getCapabilities() {
+        return (int) mCapabilities;
+    }
+
+    @Override // Binder call
     @Nullable
     public VibratorInfo getVibratorInfo(int vibratorId) {
         final VibratorController controller = mVibrators.get(vibratorId);
@@ -590,11 +610,17 @@
             logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_ERROR_TOKEN);
             return null;
         }
-        if (effect.hasVendorEffects()
-                && !hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) {
-            Slog.e(TAG, "vibrate; no permission for vendor effects");
-            logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION);
-            return null;
+        if (effect.hasVendorEffects()) {
+            if (!Flags.vendorVibrationEffects()) {
+                Slog.e(TAG, "vibrate; vendor effects feature disabled");
+                logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_UNSUPPORTED);
+                return null;
+            }
+            if (!hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) {
+                Slog.e(TAG, "vibrate; no permission for vendor effects");
+                logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION);
+                return null;
+            }
         }
         enforceUpdateAppOpsStatsPermission(uid);
         if (!isEffectValid(effect)) {
@@ -623,7 +649,7 @@
 
             // Check if ongoing vibration is more important than this vibration.
             if (ignoreStatus == null) {
-                Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(session);
+                Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session);
                 if (vibrationEndInfo != null) {
                     ignoreStatus = vibrationEndInfo.status;
                     ignoredBy = vibrationEndInfo.endedBy;
@@ -634,8 +660,8 @@
             if (ignoreStatus == null) {
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    if (mCurrentVibration != null) {
-                        if (shouldPipelineVibrationLocked(mCurrentVibration, vib)) {
+                    if (mCurrentSession != null) {
+                        if (shouldPipelineVibrationLocked(mCurrentSession, vib)) {
                             // Don't cancel the current vibration if it's pipeline-able.
                             // Note that if there is a pending next vibration that can't be
                             // pipelined, it will have already cancelled the current one, so we
@@ -645,12 +671,12 @@
                             }
                         } else {
                             vib.stats.reportInterruptedAnotherVibration(
-                                    mCurrentVibration.getCallerInfo());
-                            mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo,
+                                    mCurrentSession.getCallerInfo());
+                            mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo,
                                     /* immediate= */ false);
                         }
                     }
-                    clearNextVibrationLocked(Status.CANCELLED_SUPERSEDED, callerInfo);
+                    clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo);
                     ignoreStatus = startVibrationLocked(session);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -659,7 +685,7 @@
 
             // Ignored or failed to start the vibration, end it and report metrics right away.
             if (ignoreStatus != null) {
-                endVibrationLocked(session, ignoreStatus, ignoredBy);
+                endSessionLocked(session, ignoreStatus, ignoredBy);
             }
             return vib;
         }
@@ -681,14 +707,14 @@
                 try {
                     // TODO(b/370948466): investigate why token not checked on external vibrations.
                     IBinder cancelToken =
-                            (mNextVibration instanceof ExternalVibrationSession) ? null : token;
-                    if (shouldCancelVibration(mNextVibration, usageFilter, cancelToken)) {
-                        clearNextVibrationLocked(Status.CANCELLED_BY_USER);
+                            (mNextSession instanceof ExternalVibrationSession) ? null : token;
+                    if (shouldCancelSession(mNextSession, usageFilter, cancelToken)) {
+                        clearNextSessionLocked(Status.CANCELLED_BY_USER);
                     }
                     cancelToken =
-                            (mCurrentVibration instanceof ExternalVibrationSession) ? null : token;
-                    if (shouldCancelVibration(mCurrentVibration, usageFilter, cancelToken)) {
-                        mCurrentVibration.requestEnd(Status.CANCELLED_BY_USER);
+                            (mCurrentSession instanceof ExternalVibrationSession) ? null : token;
+                    if (shouldCancelSession(mCurrentSession, usageFilter, cancelToken)) {
+                        mCurrentSession.requestEnd(Status.CANCELLED_BY_USER);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -699,6 +725,141 @@
         }
     }
 
+    @android.annotation.EnforcePermission(allOf = {
+            android.Manifest.permission.VIBRATE,
+            android.Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+            android.Manifest.permission.START_VIBRATION_SESSIONS,
+    })
+    @Override // Binder call
+    public ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg,
+            int[] vibratorIds, VibrationAttributes attrs, String reason,
+            IVibrationSessionCallback callback) {
+        startVendorVibrationSession_enforcePermission();
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationSession");
+        try {
+            VendorVibrationSession session = startVendorVibrationSessionInternal(
+                    uid, deviceId, opPkg, vibratorIds, attrs, reason, callback);
+            return session == null ? null : session.getCancellationSignal();
+        } finally {
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    VendorVibrationSession startVendorVibrationSessionInternal(int uid, int deviceId, String opPkg,
+            int[] vibratorIds, VibrationAttributes attrs, String reason,
+            IVibrationSessionCallback callback) {
+        if (!Flags.vendorVibrationEffects()) {
+            throw new UnsupportedOperationException("Vibration sessions not supported");
+        }
+        attrs = fixupVibrationAttributes(attrs, /* effect= */ null);
+        CallerInfo callerInfo = new CallerInfo(attrs, uid, deviceId, opPkg, reason);
+        if (callback == null) {
+            Slog.e(TAG, "session callback must not be null");
+            logAndRecordSessionAttempt(callerInfo, Status.IGNORED_ERROR_TOKEN);
+            return null;
+        }
+        if (vibratorIds == null) {
+            vibratorIds = new int[0];
+        }
+        enforceUpdateAppOpsStatsPermission(uid);
+        VendorVibrationSession session = new VendorVibrationSession(callerInfo, mHandler,
+                mVendorVibrationSessionCallbacks, vibratorIds, callback);
+
+        if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) {
+            // Force update of user settings before checking if this vibration effect should
+            // be ignored or scaled.
+            mVibrationSettings.update();
+        }
+
+        synchronized (mLock) {
+            if (DEBUG) {
+                Slog.d(TAG, "Starting session " + session.getSessionId());
+            }
+
+            Status ignoreStatus = null;
+            CallerInfo ignoredBy = null;
+
+            // Check if HAL has capability to start sessions.
+            if ((mCapabilities & IVibratorManager.CAP_START_SESSIONS) == 0) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Missing capability to start sessions, ignoring request");
+                }
+                ignoreStatus = Status.IGNORED_UNSUPPORTED;
+            }
+
+            // Check if any vibrator ID was requested.
+            if (ignoreStatus == null && vibratorIds.length == 0) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Empty vibrator ids to start session, ignoring request");
+                }
+                ignoreStatus = Status.IGNORED_UNSUPPORTED;
+            }
+
+            // Check if user settings or DnD is set to ignore this session.
+            if (ignoreStatus == null) {
+                ignoreStatus = shouldIgnoreVibrationLocked(callerInfo);
+            }
+
+            // Check if ongoing vibration is more important than this session.
+            if (ignoreStatus == null) {
+                Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session);
+                if (vibrationEndInfo != null) {
+                    ignoreStatus = vibrationEndInfo.status;
+                    ignoredBy = vibrationEndInfo.endedBy;
+                }
+            }
+
+            if (ignoreStatus == null) {
+                final long ident = Binder.clearCallingIdentity();
+                try {
+                    // If not ignored so far then stop ongoing sessions before starting this one.
+                    clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo);
+                    if (mCurrentSession != null) {
+                        mNextSession = session;
+                        mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo,
+                                /* immediate= */ false);
+                    } else {
+                        ignoreStatus = startVendorSessionLocked(session);
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(ident);
+                }
+            }
+
+            // Ignored or failed to start the session, end it and report metrics right away.
+            if (ignoreStatus != null) {
+                endSessionLocked(session, ignoreStatus, ignoredBy);
+            }
+            return session;
+        }
+    }
+
+    @GuardedBy("mLock")
+    @Nullable
+    private Status startVendorSessionLocked(VendorVibrationSession session) {
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "startSessionLocked");
+        try {
+            if (session.isEnded()) {
+                // Session already ended, possibly cancelled by app cancellation signal.
+                return session.getStatus();
+            }
+            if (!session.linkToDeath()) {
+                return Status.IGNORED_ERROR_TOKEN;
+            }
+            if (!mNativeWrapper.startSession(session.getSessionId(), session.getVibratorIds())) {
+                Slog.e(TAG, "Error starting session " + session.getSessionId()
+                        + " on vibrators " + Arrays.toString(session.getVibratorIds()));
+                session.unlinkToDeath();
+                return Status.IGNORED_UNSUPPORTED;
+            }
+            session.notifyStart();
+            mCurrentSession = session;
+            return null;
+        } finally {
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
+        }
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -747,8 +908,8 @@
 
             pw.println("CurrentVibration:");
             pw.increaseIndent();
-            if (mCurrentVibration != null) {
-                mCurrentVibration.getDebugInfo().dump(pw);
+            if (mCurrentSession != null) {
+                mCurrentSession.getDebugInfo().dump(pw);
             } else {
                 pw.println("null");
             }
@@ -757,8 +918,8 @@
 
             pw.println("NextVibration:");
             pw.increaseIndent();
-            if (mNextVibration != null) {
-                mNextVibration.getDebugInfo().dump(pw);
+            if (mNextSession != null) {
+                mNextSession.getDebugInfo().dump(pw);
             } else {
                 pw.println("null");
             }
@@ -782,8 +943,8 @@
         synchronized (mLock) {
             mVibrationSettings.dump(proto);
             mVibrationScaler.dump(proto);
-            if (mCurrentVibration != null) {
-                mCurrentVibration.getDebugInfo().dump(proto,
+            if (mCurrentSession != null) {
+                mCurrentSession.getDebugInfo().dump(proto,
                         VibratorManagerServiceDumpProto.CURRENT_VIBRATION);
             }
             for (int i = 0; i < mVibrators.size(); i++) {
@@ -816,18 +977,18 @@
             }
 
             // TODO(b/372241975): investigate why external vibrations were not handled here before
-            if (mCurrentVibration == null
-                    || (mCurrentVibration instanceof ExternalVibrationSession)) {
+            if (mCurrentSession == null
+                    || (mCurrentSession instanceof ExternalVibrationSession)) {
                 return;
             }
 
-            Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentVibration.getCallerInfo());
+            Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentSession.getCallerInfo());
             if (inputDevicesChanged || (ignoreStatus != null)) {
                 if (DEBUG) {
                     Slog.d(TAG, "Canceling vibration because settings changed: "
                             + (inputDevicesChanged ? "input devices changed" : ignoreStatus));
                 }
-                mCurrentVibration.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE);
+                mCurrentSession.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE);
             }
         }
     }
@@ -866,15 +1027,15 @@
             if (mInputDeviceDelegate.isAvailable()) {
                 return startVibrationOnInputDevicesLocked(session.getVibration());
             }
-            if (mCurrentVibration == null) {
+            if (mCurrentSession == null) {
                 return startVibrationOnThreadLocked(session);
             }
             // If there's already a vibration queued (waiting for the previous one to finish
             // cancelling), end it cleanly and replace it with the new one.
             // Note that we don't consider pipelining here, because new pipelined ones should
             // replace pending non-executing pipelined ones anyway.
-            clearNextVibrationLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo());
-            mNextVibration = session;
+            clearNextSessionLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo());
+            mNextSession = session;
             return null;
         } finally {
             Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -891,16 +1052,16 @@
             case AppOpsManager.MODE_ALLOWED:
                 Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0);
                 // Make sure mCurrentVibration is set while triggering the VibrationThread.
-                mCurrentVibration = session;
-                if (!mCurrentVibration.linkToDeath()) {
+                mCurrentSession = session;
+                if (!mCurrentSession.linkToDeath()) {
                     // Shouldn't happen. The method call already logs.
-                    mCurrentVibration = null;  // Aborted.
+                    mCurrentSession = null;  // Aborted.
                     return Status.IGNORED_ERROR_TOKEN;
                 }
                 if (!mVibrationThread.runVibrationOnVibrationThread(conductor)) {
                     // Shouldn't happen. The method call already logs.
                     session.setVibrationConductor(null); // Rejected by thread, clear it in session.
-                    mCurrentVibration = null;  // Aborted.
+                    mCurrentSession = null;  // Aborted.
                     return Status.IGNORED_ERROR_SCHEDULING;
                 }
                 return null;
@@ -914,23 +1075,29 @@
     }
 
     @GuardedBy("mLock")
-    private void maybeStartNextSingleVibrationLocked() {
-        if (mNextVibration instanceof SingleVibrationSession session) {
-            mNextVibration = null;
+    private void maybeStartNextSessionLocked() {
+        if (mNextSession instanceof SingleVibrationSession session) {
+            mNextSession = null;
             Status errorStatus = startVibrationOnThreadLocked(session);
             if (errorStatus != null) {
-                endVibrationLocked(session, errorStatus);
+                endSessionLocked(session, errorStatus);
             }
-        }
+        } else if (mNextSession instanceof VendorVibrationSession session) {
+            mNextSession = null;
+            Status errorStatus = startVendorSessionLocked(session);
+            if (errorStatus != null) {
+                endSessionLocked(session, errorStatus);
+            }
+        } // External vibrations cannot be started asynchronously.
     }
 
     @GuardedBy("mLock")
-    private void endVibrationLocked(VibrationSession session, Status status) {
-        endVibrationLocked(session, status, /* endedBy= */ null);
+    private void endSessionLocked(VibrationSession session, Status status) {
+        endSessionLocked(session, status, /* endedBy= */ null);
     }
 
     @GuardedBy("mLock")
-    private void endVibrationLocked(VibrationSession session, Status status, CallerInfo endedBy) {
+    private void endSessionLocked(VibrationSession session, Status status, CallerInfo endedBy) {
         session.requestEnd(status, endedBy, /* immediate= */ false);
         logAndRecordVibration(session.getDebugInfo());
     }
@@ -975,6 +1142,13 @@
                         VibrationScaler.ADAPTIVE_SCALE_NONE));
     }
 
+    private void logAndRecordSessionAttempt(CallerInfo callerInfo, Status status) {
+        logAndRecordVibration(
+                new VendorVibrationSession.DebugInfoImpl(status, callerInfo,
+                        SystemClock.uptimeMillis(), System.currentTimeMillis(),
+                        /* startTime= */ 0, /* endUptime= */ 0, /* endTime= */ 0));
+    }
+
     private void logAndRecordVibration(DebugInfo info) {
         info.logMetrics(mFrameworkStatsLogger);
         logVibrationStatus(info.getCallerInfo().uid, info.getCallerInfo().attrs, info.getStatus());
@@ -1026,25 +1200,40 @@
         }
     }
 
+    private void onVibrationSessionComplete(long sessionId) {
+        synchronized (mLock) {
+            if (mCurrentSession == null || mCurrentSession.getSessionId() != sessionId) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Vibration session " + sessionId + " callback ignored");
+                }
+                return;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "Vibration session " + sessionId + " complete, notifying session");
+            }
+            mCurrentSession.notifySessionCallback();
+        }
+    }
+
     private void onSyncedVibrationComplete(long vibrationId) {
         synchronized (mLock) {
-            if (mCurrentVibration != null) {
+            if (mCurrentSession != null) {
                 if (DEBUG) {
                     Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread");
                 }
-                mCurrentVibration.notifySyncedVibratorsCallback(vibrationId);
+                mCurrentSession.notifySyncedVibratorsCallback(vibrationId);
             }
         }
     }
 
     private void onVibrationComplete(int vibratorId, long vibrationId) {
         synchronized (mLock) {
-            if (mCurrentVibration != null) {
+            if (mCurrentSession != null) {
                 if (DEBUG) {
                     Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId
                             + " complete, notifying thread");
                 }
-                mCurrentVibration.notifyVibratorCallback(vibratorId, vibrationId);
+                mCurrentSession.notifyVibratorCallback(vibratorId, vibrationId);
             }
         }
     }
@@ -1056,10 +1245,10 @@
      */
     @GuardedBy("mLock")
     @Nullable
-    private Vibration.EndInfo shouldIgnoreVibrationForOngoingLocked(VibrationSession session) {
-        if (mNextVibration != null) {
-            Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoing(session,
-                    mNextVibration);
+    private Vibration.EndInfo shouldIgnoreForOngoingLocked(VibrationSession session) {
+        if (mNextSession != null) {
+            Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoing(session,
+                    mNextSession);
             if (vibrationEndInfo != null) {
                 // Next vibration has higher importance than the new one, so the new vibration
                 // should be ignored.
@@ -1067,13 +1256,13 @@
             }
         }
 
-        if (mCurrentVibration != null) {
-            if (mCurrentVibration.wasEndRequested()) {
+        if (mCurrentSession != null) {
+            if (mCurrentSession.wasEndRequested()) {
                 // Current session has ended or is cancelling, should not block incoming vibrations.
                 return null;
             }
 
-            return shouldIgnoreVibrationForOngoing(session, mCurrentVibration);
+            return shouldIgnoreForOngoing(session, mCurrentSession);
         }
 
         return null;
@@ -1086,7 +1275,7 @@
      * @return a Vibration.EndInfo if the vibration should be ignored, null otherwise.
      */
     @Nullable
-    private static Vibration.EndInfo shouldIgnoreVibrationForOngoing(
+    private static Vibration.EndInfo shouldIgnoreForOngoing(
             @NonNull VibrationSession newSession, @NonNull VibrationSession ongoingSession) {
 
         int newSessionImportance = getVibrationImportance(newSession);
@@ -1214,11 +1403,15 @@
      * @param tokenFilter The binder token to identify the vibration origin. Only vibrations
      *                    started with the same token can be cancelled with it.
      */
-    private boolean shouldCancelVibration(@Nullable VibrationSession session, int usageFilter,
+    private boolean shouldCancelSession(@Nullable VibrationSession session, int usageFilter,
             @Nullable IBinder tokenFilter) {
         if (session == null) {
             return false;
         }
+        if (session instanceof VendorVibrationSession) {
+            // Vendor sessions should not be cancelled by Vibrator.cancel API.
+            return false;
+        }
         if ((tokenFilter != null) && (tokenFilter != session.getCallerToken())) {
             // Vibration from a different app, this should not cancel it.
             return false;
@@ -1572,10 +1765,10 @@
             Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased");
             try {
                 synchronized (mLock) {
-                    if (!(mCurrentVibration instanceof SingleVibrationSession session)) {
+                    if (!(mCurrentSession instanceof SingleVibrationSession session)) {
                         if (Build.IS_DEBUGGABLE) {
                             Slog.wtf(TAG, "VibrationSession invalid on vibration thread release."
-                                    + " currentSession=" + mCurrentVibration);
+                                    + " currentSession=" + mCurrentSession);
                         }
                         // Only single vibration sessions are ended by thread being released. Abort.
                         return;
@@ -1586,11 +1779,11 @@
                                         + " expected=%d, released=%d",
                                 session.getVibration().id, vibrationId));
                     }
-                    finishAppOpModeLocked(mCurrentVibration.getCallerInfo());
-                    clearCurrentVibrationLocked();
+                    finishAppOpModeLocked(mCurrentSession.getCallerInfo());
+                    clearCurrentSessionLocked();
                     Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
-                    // Start next vibration if it's a single vibration waiting for the thread.
-                    maybeStartNextSingleVibrationLocked();
+                    // Start next vibration if it's waiting for the thread.
+                    maybeStartNextSessionLocked();
                 }
             } finally {
                 Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -1613,10 +1806,10 @@
             Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationReleased");
             try {
                 synchronized (mLock) {
-                    if (!(mCurrentVibration instanceof ExternalVibrationSession session)) {
+                    if (!(mCurrentSession instanceof ExternalVibrationSession session)) {
                         if (Build.IS_DEBUGGABLE) {
                             Slog.wtf(TAG, "VibrationSession invalid on external vibration release."
-                                    + " currentSession=" + mCurrentVibration);
+                                    + " currentSession=" + mCurrentSession);
                         }
                         // Only external vibration sessions are ended by this callback. Abort.
                         return;
@@ -1627,10 +1820,9 @@
                                         + " expected=%d, released=%d", session.id, vibrationId));
                     }
                     setExternalControl(false, session.stats);
-                    clearCurrentVibrationLocked();
-                    // Start next vibration if it's a single vibration waiting for the external
-                    // control to be over.
-                    maybeStartNextSingleVibrationLocked();
+                    clearCurrentSessionLocked();
+                    // Start next vibration if it's waiting for the external control to be over.
+                    maybeStartNextSessionLocked();
                 }
             } finally {
                 Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -1638,19 +1830,75 @@
         }
     }
 
-    /** Listener for synced vibration completion callbacks from native. */
+    /**
+     * Implementation of {@link ExternalVibrationSession.VibratorManagerHooks} that controls
+     * external vibrations and reports them when finished.
+     */
+    private final class VendorVibrationSessionCallbacks
+            implements VendorVibrationSession.VibratorManagerHooks {
+
+        @Override
+        public void endSession(long sessionId, boolean shouldAbort) {
+            if (DEBUG) {
+                Slog.d(TAG, "Vibration session " + sessionId
+                        + (shouldAbort ? " aborting" : " ending"));
+            }
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "endSession");
+            try {
+                mNativeWrapper.endSession(sessionId, shouldAbort);
+            } finally {
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
+            }
+        }
+
+        @Override
+        public void onSessionReleased(long sessionId) {
+            if (DEBUG) {
+                Slog.d(TAG, "Vibration session " + sessionId + " released");
+            }
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVendorSessionReleased");
+            try {
+                synchronized (mLock) {
+                    if (!(mCurrentSession instanceof VendorVibrationSession session)) {
+                        if (Build.IS_DEBUGGABLE) {
+                            Slog.wtf(TAG, "VibrationSession invalid on vibration session release."
+                                    + " currentSession=" + mCurrentSession);
+                        }
+                        // Only vendor vibration sessions are ended by this callback. Abort.
+                        return;
+                    }
+                    if (Build.IS_DEBUGGABLE && (session.getSessionId() != sessionId)) {
+                        Slog.wtf(TAG, TextUtils.formatSimple(
+                                "SessionId mismatch on vendor vibration session release."
+                                        + " expected=%d, released=%d",
+                                session.getSessionId(), sessionId));
+                    }
+                    clearCurrentSessionLocked();
+                    // Start next vibration if it's waiting for the HAL session to be over.
+                    maybeStartNextSessionLocked();
+                }
+            } finally {
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
+            }
+        }
+    }
+
+    /** Listener for vibrator manager completion callbacks from native. */
     @VisibleForTesting
-    interface OnSyncedVibrationCompleteListener {
+    interface VibratorManagerNativeCallbacks {
 
         /** Callback triggered when synced vibration is complete. */
-        void onComplete(long vibrationId);
+        void onSyncedVibrationComplete(long vibrationId);
+
+        /** Callback triggered when vibration session is complete. */
+        void onVibrationSessionComplete(long sessionId);
     }
 
     /**
      * Implementation of listeners to native vibrators with a weak reference to this service.
      */
     private static final class VibrationCompleteListener implements
-            VibratorController.OnVibrationCompleteListener, OnSyncedVibrationCompleteListener {
+            VibratorController.OnVibrationCompleteListener, VibratorManagerNativeCallbacks {
         private WeakReference<VibratorManagerService> mServiceRef;
 
         VibrationCompleteListener(VibratorManagerService service) {
@@ -1658,7 +1906,7 @@
         }
 
         @Override
-        public void onComplete(long vibrationId) {
+        public void onSyncedVibrationComplete(long vibrationId) {
             VibratorManagerService service = mServiceRef.get();
             if (service != null) {
                 service.onSyncedVibrationComplete(vibrationId);
@@ -1666,6 +1914,14 @@
         }
 
         @Override
+        public void onVibrationSessionComplete(long sessionId) {
+            VibratorManagerService service = mServiceRef.get();
+            if (service != null) {
+                service.onVibrationSessionComplete(sessionId);
+            }
+        }
+
+        @Override
         public void onComplete(int vibratorId, long vibrationId) {
             VibratorManagerService service = mServiceRef.get();
             if (service != null) {
@@ -1698,7 +1954,7 @@
         private long mNativeServicePtr = 0;
 
         /** Returns native pointer to newly created controller and connects with HAL service. */
-        public void init(OnSyncedVibrationCompleteListener listener) {
+        public void init(VibratorManagerNativeCallbacks listener) {
             mNativeServicePtr = nativeInit(listener);
             long finalizerPtr = nativeGetFinalizer();
 
@@ -1734,6 +1990,21 @@
         public void cancelSynced() {
             nativeCancelSynced(mNativeServicePtr);
         }
+
+        /** Start vibration session. */
+        public boolean startSession(long sessionId, @NonNull int[] vibratorIds) {
+            return nativeStartSession(mNativeServicePtr, sessionId, vibratorIds);
+        }
+
+        /** End vibration session. */
+        public void endSession(long sessionId, boolean shouldAbort) {
+            nativeEndSession(mNativeServicePtr, sessionId, shouldAbort);
+        }
+
+        /** Clear vibration sessions. */
+        public void clearSessions() {
+            nativeClearSessions(mNativeServicePtr);
+        }
     }
 
     /** Keep records of vibrations played and provide debug information for this service. */
@@ -1853,46 +2124,46 @@
 
     /** Clears mNextVibration if set, ending it cleanly */
     @GuardedBy("mLock")
-    private void clearNextVibrationLocked(Status status) {
-        clearNextVibrationLocked(status, /* endedBy= */ null);
+    private void clearNextSessionLocked(Status status) {
+        clearNextSessionLocked(status, /* endedBy= */ null);
     }
 
     /** Clears mNextVibration if set, ending it cleanly */
     @GuardedBy("mLock")
-    private void clearNextVibrationLocked(Status status, CallerInfo endedBy) {
-        if (mNextVibration != null) {
+    private void clearNextSessionLocked(Status status, CallerInfo endedBy) {
+        if (mNextSession != null) {
             if (DEBUG) {
-                Slog.d(TAG, "Dropping pending vibration from " + mNextVibration.getCallerInfo()
+                Slog.d(TAG, "Dropping pending vibration from " + mNextSession.getCallerInfo()
                         + " with status: " + status);
             }
             // Clearing next vibration before playing it, end it and report metrics right away.
-            endVibrationLocked(mNextVibration, status, endedBy);
-            mNextVibration = null;
+            endSessionLocked(mNextSession, status, endedBy);
+            mNextSession = null;
         }
     }
 
     /** Clears mCurrentVibration if set, reporting metrics */
     @GuardedBy("mLock")
-    private void clearCurrentVibrationLocked() {
-        if (mCurrentVibration != null) {
-            mCurrentVibration.unlinkToDeath();
-            logAndRecordVibration(mCurrentVibration.getDebugInfo());
-            mCurrentVibration = null;
+    private void clearCurrentSessionLocked() {
+        if (mCurrentSession != null) {
+            mCurrentSession.unlinkToDeath();
+            logAndRecordVibration(mCurrentSession.getDebugInfo());
+            mCurrentSession = null;
             mLock.notify(); // Notify if waiting for current vibration to end.
         }
     }
 
     @GuardedBy("mLock")
-    private void maybeClearCurrentAndNextVibrationsLocked(
+    private void maybeClearCurrentAndNextSessionsLocked(
             Predicate<VibrationSession> shouldEndSessionPredicate, Status endStatus) {
         // TODO(b/372241975): investigate why external vibrations were not handled here before
-        if (!(mNextVibration instanceof ExternalVibrationSession)
-                && shouldEndSessionPredicate.test(mNextVibration)) {
-            clearNextVibrationLocked(endStatus);
+        if (!(mNextSession instanceof ExternalVibrationSession)
+                && shouldEndSessionPredicate.test(mNextSession)) {
+            clearNextSessionLocked(endStatus);
         }
-        if (!(mCurrentVibration instanceof ExternalVibrationSession)
-                && shouldEndSessionPredicate.test(mCurrentVibration)) {
-            mCurrentVibration.requestEnd(endStatus);
+        if (!(mCurrentSession instanceof ExternalVibrationSession)
+                && shouldEndSessionPredicate.test(mCurrentSession)) {
+            mCurrentSession.requestEnd(endStatus);
         }
     }
 
@@ -1902,12 +2173,12 @@
      *
      * @return true if the vibration completed, or false if waiting timed out.
      */
-    public boolean waitForCurrentVibrationEnd(long maxWaitMillis) {
+    public boolean waitForCurrentSessionEnd(long maxWaitMillis) {
         long now = SystemClock.elapsedRealtime();
         long deadline = now + maxWaitMillis;
         synchronized (mLock) {
             while (true) {
-                if (mCurrentVibration == null) {
+                if (mCurrentSession == null) {
                     return true;  // Done
                 }
                 if (now >= deadline) {  // Note that thread.wait(0) waits indefinitely.
@@ -1965,7 +2236,7 @@
 
                 synchronized (mLock) {
                     if (!hasExternalControlCapability()) {
-                        endVibrationLocked(session, Status.IGNORED_UNSUPPORTED);
+                        endSessionLocked(session, Status.IGNORED_UNSUPPORTED);
                         return session.getScale();
                     }
 
@@ -1976,17 +2247,17 @@
                         Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
                                 + " tried to play externally controlled vibration"
                                 + " without VIBRATE permission, ignoring.");
-                        endVibrationLocked(session, Status.IGNORED_MISSING_PERMISSION);
+                        endSessionLocked(session, Status.IGNORED_MISSING_PERMISSION);
                         return session.getScale();
                     }
 
                     Status ignoreStatus = shouldIgnoreVibrationLocked(session.callerInfo);
                     if (ignoreStatus != null) {
-                        endVibrationLocked(session, ignoreStatus);
+                        endSessionLocked(session, ignoreStatus);
                         return session.getScale();
                     }
 
-                    if ((mCurrentVibration instanceof ExternalVibrationSession evs)
+                    if ((mCurrentSession instanceof ExternalVibrationSession evs)
                             && evs.isHoldingSameVibration(vib)) {
                         // We are already playing this external vibration, so we can return the same
                         // scale calculated in the previous call to this method.
@@ -1994,17 +2265,17 @@
                     }
 
                     // Check if ongoing vibration is more important than this vibration.
-                    Vibration.EndInfo ignoreInfo = shouldIgnoreVibrationForOngoingLocked(session);
+                    Vibration.EndInfo ignoreInfo = shouldIgnoreForOngoingLocked(session);
                     if (ignoreInfo != null) {
-                        endVibrationLocked(session, ignoreInfo.status, ignoreInfo.endedBy);
+                        endSessionLocked(session, ignoreInfo.status, ignoreInfo.endedBy);
                         return session.getScale();
                     }
 
                     // First clear next request, so it won't start when the current one ends.
-                    clearNextVibrationLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo);
-                    mNextVibration = session;
+                    clearNextSessionLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo);
+                    mNextSession = session;
 
-                    if (mCurrentVibration != null) {
+                    if (mCurrentSession != null) {
                         // Cancel any vibration that may be playing and ready the vibrator, even if
                         // we have an externally controlled vibration playing already.
                         // Since the interface defines that only one externally controlled
@@ -2016,36 +2287,36 @@
                         // as we would need to mute the old one still if it came from a different
                         // controller.
                         session.stats.reportInterruptedAnotherVibration(
-                                mCurrentVibration.getCallerInfo());
-                        mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED,
+                                mCurrentSession.getCallerInfo());
+                        mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED,
                                 session.callerInfo, /* immediate= */ true);
                         waitForCompletion = true;
                     }
                 }
                 // Wait for lock and interact with HAL to set external control outside main lock.
                 if (waitForCompletion) {
-                    if (!waitForCurrentVibrationEnd(VIBRATION_CANCEL_WAIT_MILLIS)) {
+                    if (!waitForCurrentSessionEnd(VIBRATION_CANCEL_WAIT_MILLIS)) {
                         Slog.e(TAG, "Timed out waiting for vibration to cancel");
                         synchronized (mLock) {
-                            if (mNextVibration == session) {
-                                mNextVibration = null;
+                            if (mNextSession == session) {
+                                mNextSession = null;
                             }
-                            endVibrationLocked(session, Status.IGNORED_ERROR_CANCELLING);
+                            endSessionLocked(session, Status.IGNORED_ERROR_CANCELLING);
                             return session.getScale();
                         }
                     }
                 }
                 synchronized (mLock) {
-                    if (mNextVibration == session) {
+                    if (mNextSession == session) {
                         // This is still the next vibration to be played.
-                        mNextVibration = null;
+                        mNextSession = null;
                     } else {
                         // A new request took the place of this one, maybe with higher importance.
                         // Next vibration already cleared with the right status, just return here.
                         return session.getScale();
                     }
                     if (!session.linkToDeath()) {
-                        endVibrationLocked(session, Status.IGNORED_ERROR_TOKEN);
+                        endSessionLocked(session, Status.IGNORED_ERROR_TOKEN);
                         return session.getScale();
                     }
                     if (DEBUG) {
@@ -2062,7 +2333,7 @@
                         // should be ignored or scaled.
                         mVibrationSettings.update();
                     }
-                    mCurrentVibration = session;
+                    mCurrentSession = session;
                     session.scale(mVibrationScaler, attrs.getUsage());
 
                     // Vibrator will start receiving data from external channels after this point.
@@ -2080,12 +2351,12 @@
             Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
             try {
                 synchronized (mLock) {
-                    if ((mCurrentVibration instanceof ExternalVibrationSession evs)
+                    if ((mCurrentSession instanceof ExternalVibrationSession evs)
                             && evs.isHoldingSameVibration(vib)) {
                         if (DEBUG) {
                             Slog.d(TAG, "Stopping external vibration: " + vib);
                         }
-                        mCurrentVibration.requestEnd(Status.FINISHED);
+                        mCurrentSession.requestEnd(Status.FINISHED);
                     }
                 }
             } finally {
diff --git a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp
index a47ab9d..46be79e 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp
@@ -16,27 +16,32 @@
 
 #define LOG_TAG "VibratorManagerService"
 
+#include "com_android_server_vibrator_VibratorManagerService.h"
+
 #include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+#include <utils/misc.h>
+#include <vibratorservice/VibratorManagerHalController.h>
+
+#include <unordered_map>
+
 #include "android_runtime/AndroidRuntime.h"
 #include "core_jni_helpers.h"
 #include "jni.h"
 
-#include <utils/Log.h>
-#include <utils/misc.h>
-
-#include <vibratorservice/VibratorManagerHalController.h>
-
-#include "com_android_server_vibrator_VibratorManagerService.h"
-
 namespace android {
 
 static JavaVM* sJvm = nullptr;
-static jmethodID sMethodIdOnComplete;
+static jmethodID sMethodIdOnSyncedVibrationComplete;
+static jmethodID sMethodIdOnVibrationSessionComplete;
 static std::mutex gManagerMutex;
 static vibrator::ManagerHalController* gManager GUARDED_BY(gManagerMutex) = nullptr;
 
 class NativeVibratorManagerService {
 public:
+    using IVibrationSession = aidl::android::hardware::vibrator::IVibrationSession;
+    using VibrationSessionConfig = aidl::android::hardware::vibrator::VibrationSessionConfig;
+
     NativeVibratorManagerService(JNIEnv* env, jobject callbackListener)
           : mHal(std::make_unique<vibrator::ManagerHalController>()),
             mCallbackListener(env->NewGlobalRef(callbackListener)) {
@@ -52,15 +57,69 @@
 
     vibrator::ManagerHalController* hal() const { return mHal.get(); }
 
-    std::function<void()> createCallback(jlong vibrationId) {
+    std::function<void()> createSyncedVibrationCallback(jlong vibrationId) {
         return [vibrationId, this]() {
             auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
-            jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, vibrationId);
+            jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnSyncedVibrationComplete,
+                                   vibrationId);
         };
     }
 
+    std::function<void()> createVibrationSessionCallback(jlong sessionId) {
+        return [sessionId, this]() {
+            auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
+            jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnVibrationSessionComplete,
+                                   sessionId);
+            std::lock_guard<std::mutex> lock(mSessionMutex);
+            auto it = mSessions.find(sessionId);
+            if (it != mSessions.end()) {
+                mSessions.erase(it);
+            }
+        };
+    }
+
+    bool startSession(jlong sessionId, const std::vector<int32_t>& vibratorIds) {
+        VibrationSessionConfig config;
+        auto callback = createVibrationSessionCallback(sessionId);
+        auto result = hal()->startSession(vibratorIds, config, callback);
+        if (!result.isOk()) {
+            return false;
+        }
+
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        mSessions[sessionId] = std::move(result.value());
+        return true;
+    }
+
+    void closeSession(jlong sessionId) {
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        auto it = mSessions.find(sessionId);
+        if (it != mSessions.end()) {
+            it->second->close();
+            // Keep session, it can still be aborted.
+        }
+    }
+
+    void abortSession(jlong sessionId) {
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        auto it = mSessions.find(sessionId);
+        if (it != mSessions.end()) {
+            it->second->abort();
+            mSessions.erase(it);
+        }
+    }
+
+    void clearSessions() {
+        hal()->clearSessions();
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        mSessions.clear();
+    }
+
 private:
+    std::mutex mSessionMutex;
     const std::unique_ptr<vibrator::ManagerHalController> mHal;
+    std::unordered_map<jlong, std::shared_ptr<IVibrationSession>> mSessions
+            GUARDED_BY(mSessionMutex);
     const jobject mCallbackListener;
 };
 
@@ -142,7 +201,7 @@
         ALOGE("nativeTriggerSynced failed because native service was not initialized");
         return JNI_FALSE;
     }
-    auto callback = service->createCallback(vibrationId);
+    auto callback = service->createSyncedVibrationCallback(vibrationId);
     return service->hal()->triggerSynced(callback).isOk() ? JNI_TRUE : JNI_FALSE;
 }
 
@@ -156,8 +215,47 @@
     service->hal()->cancelSynced();
 }
 
+static jboolean nativeStartSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr,
+                                   jlong sessionId, jintArray vibratorIds) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeStartSession failed because native service was not initialized");
+        return JNI_FALSE;
+    }
+    jsize size = env->GetArrayLength(vibratorIds);
+    std::vector<int32_t> ids(size);
+    env->GetIntArrayRegion(vibratorIds, 0, size, reinterpret_cast<jint*>(ids.data()));
+    return service->startSession(sessionId, ids) ? JNI_TRUE : JNI_FALSE;
+}
+
+static void nativeEndSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr, jlong sessionId,
+                             jboolean shouldAbort) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeEndSession failed because native service was not initialized");
+        return;
+    }
+    if (shouldAbort) {
+        service->abortSession(sessionId);
+    } else {
+        service->closeSession(sessionId);
+    }
+}
+
+static void nativeClearSessions(JNIEnv* env, jclass /* clazz */, jlong servicePtr) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeClearSessions failed because native service was not initialized");
+        return;
+    }
+    service->clearSessions();
+}
+
 inline static constexpr auto sNativeInitMethodSignature =
-        "(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)J";
+        "(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)J";
 
 static const JNINativeMethod method_table[] = {
         {"nativeInit", sNativeInitMethodSignature, (void*)nativeInit},
@@ -167,15 +265,20 @@
         {"nativePrepareSynced", "(J[I)Z", (void*)nativePrepareSynced},
         {"nativeTriggerSynced", "(JJ)Z", (void*)nativeTriggerSynced},
         {"nativeCancelSynced", "(J)V", (void*)nativeCancelSynced},
+        {"nativeStartSession", "(JJ[I)Z", (void*)nativeStartSession},
+        {"nativeEndSession", "(JJZ)V", (void*)nativeEndSession},
+        {"nativeClearSessions", "(J)V", (void*)nativeClearSessions},
 };
 
 int register_android_server_vibrator_VibratorManagerService(JavaVM* jvm, JNIEnv* env) {
     sJvm = jvm;
     auto listenerClassName =
-            "com/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener";
+            "com/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks";
     jclass listenerClass = FindClassOrDie(env, listenerClassName);
-    sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(J)V");
-
+    sMethodIdOnSyncedVibrationComplete =
+            GetMethodIDOrDie(env, listenerClass, "onSyncedVibrationComplete", "(J)V");
+    sMethodIdOnVibrationSessionComplete =
+            GetMethodIDOrDie(env, listenerClass, "onVibrationSessionComplete", "(J)V");
     return jniRegisterNativeMethods(env, "com/android/server/vibrator/VibratorManagerService",
                                     method_table, NELEM(method_table));
 }
diff --git a/services/proguard.flags b/services/proguard.flags
index cdd41ab..977bd19 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -82,7 +82,7 @@
 -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaJackDetector { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaMidiDevice { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorController$OnVibrationCompleteListener { *; }
--keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$OnSyncedVibrationCompleteListener { *; }
+-keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$VibratorManagerNativeCallbacks { *; }
 -keepclasseswithmembers,allowoptimization,allowaccessmodification class com.android.server.** {
   *** *FromNative(...);
 }
diff --git a/services/tests/vibrator/AndroidManifest.xml b/services/tests/vibrator/AndroidManifest.xml
index c0f514f..850884f 100644
--- a/services/tests/vibrator/AndroidManifest.xml
+++ b/services/tests/vibrator/AndroidManifest.xml
@@ -32,6 +32,9 @@
     <uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON" />
     <!-- Required to play system-only haptic feedback constants -->
     <uses-permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS" />
+    <!-- Required to play vendor effects and start vendor sessions -->
+    <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" />
+    <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" />
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.mock" android:required="true" />
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index dfdd0cd..88ba9e3 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -83,6 +84,8 @@
 import android.os.VibratorInfo;
 import android.os.test.FakeVibrator;
 import android.os.test.TestLooper;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.StepSegment;
@@ -195,6 +198,7 @@
             new SparseArray<>();
 
     private final List<HalVibration> mPendingVibrations = new ArrayList<>();
+    private final List<VendorVibrationSession> mPendingSessions = new ArrayList<>();
 
     private VibratorManagerService mService;
     private Context mContextSpy;
@@ -264,6 +268,11 @@
             grantPermission(android.Manifest.permission.VIBRATE);
             // Cancel any pending vibration from tests, including external vibrations.
             cancelVibrate(mService);
+            // End pending sessions.
+            for (VendorVibrationSession session : mPendingSessions) {
+                session.cancelSession();
+            }
+            mTestLooper.dispatchAll();
             // Wait until pending vibrations end asynchronously.
             for (HalVibration vibration : mPendingVibrations) {
                 vibration.waitForEnd();
@@ -1229,6 +1238,36 @@
                 .anyMatch(PrebakedSegment.class::isInstance));
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void vibrate_withOngoingHigherImportanceSession_ignoresEffect() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        HalVibration vibration = vibrateAndWaitUntilFinished(service,
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                HAPTIC_FEEDBACK_ATTRS);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        assertThat(vibration.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE);
+        verify(callback, never()).onFinishing();
+        verify(callback, never()).onFinished(anyInt());
+        // The second vibration shouldn't have played any prebaked segment.
+        assertFalse(fakeVibrator.getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
+    }
+
     @Test
     public void vibrate_withOngoingLowerImportanceVibration_cancelsOngoingEffect()
             throws Exception {
@@ -1289,6 +1328,36 @@
                 .filter(PrebakedSegment.class::isInstance).count());
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void vibrate_withOngoingLowerImportanceSession_cancelsOngoingSession() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        HalVibration vibration = vibrateAndWaitUntilFinished(service,
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                HAPTIC_FEEDBACK_ATTRS);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
+        assertThat(vibration.getStatus()).isEqualTo(Status.FINISHED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+        // One segment played is the prebaked CLICK from the new vibration.
+        assertEquals(1, mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .filter(PrebakedSegment.class::isInstance).count());
+    }
+
     @Test
     public void vibrate_withOngoingSameImportancePipelinedVibration_continuesOngoingEffect()
             throws Exception {
@@ -1416,16 +1485,16 @@
         // The native callback will be dispatched manually in this test.
         mTestLooper.stopAutoDispatchAndIgnoreExceptions();
 
-        ArgumentCaptor<VibratorManagerService.OnSyncedVibrationCompleteListener> listenerCaptor =
+        ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor =
                 ArgumentCaptor.forClass(
-                        VibratorManagerService.OnSyncedVibrationCompleteListener.class);
+                        VibratorManagerService.VibratorManagerNativeCallbacks.class);
         verify(mNativeWrapperMock).init(listenerCaptor.capture());
 
         CountDownLatch triggerCountDown = new CountDownLatch(1);
         // Mock trigger callback on registered listener right after the synced vibration starts.
         when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true);
         when(mNativeWrapperMock.triggerSynced(anyLong())).then(answer -> {
-            listenerCaptor.getValue().onComplete(answer.getArgument(0));
+            listenerCaptor.getValue().onSyncedVibrationComplete(answer.getArgument(0));
             triggerCountDown.countDown();
             return true;
         });
@@ -2318,6 +2387,34 @@
         assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates());
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void onExternalVibration_withOngoingHigherImportanceSession_ignoreNewVibration()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class));
+        ExternalVibrationScale scale =
+                mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        // External vibration is ignored.
+        assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+
+        // Session still running.
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(callback, never()).onFinishing();
+        verify(callback, never()).onFinished(anyInt());
+    }
+
     @Test
     public void onExternalVibration_withNewSameImportanceButRepeating_cancelsOngoingVibration()
             throws Exception {
@@ -2373,6 +2470,36 @@
         assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates());
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void onExternalVibration_withOngoingLowerImportanceSession_cancelsOngoingSession()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class));
+        ExternalVibrationScale scale =
+                mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+        mTestLooper.dispatchAll();
+
+        // Session is cancelled.
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+        assertEquals(Arrays.asList(false, true),
+                mVibratorProviders.get(1).getExternalControlStates());
+    }
+
     @Test
     public void onExternalVibration_withRingtone_usesRingerModeSettings() {
         mockVibrators(1);
@@ -2638,6 +2765,376 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutFeatureFlag_throwsException() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        int vibratorId = 1;
+        mockVibrators(vibratorId);
+        VibratorManagerService service = createSystemReadyService();
+
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        assertThrows("Expected starting session without feature flag to fail!",
+                UnsupportedOperationException.class,
+                () -> startSession(service, RINGTONE_ATTRS, callback, vibratorId));
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+        verify(callback, never()).onStarted(any(IVibrationSession.class));
+        verify(callback, never()).onFinishing();
+        verify(callback, never()).onFinished(anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutCapability_doesNotStart() throws Exception {
+        int vibratorId = 1;
+        mockVibrators(vibratorId);
+        VibratorManagerService service = createSystemReadyService();
+
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS,
+                callback, vibratorId);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+        verify(callback, never()).onFinishing();
+        verify(callback)
+                .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutCallback_doesNotStart() {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        int vibratorId = 1;
+        mockVibrators(vibratorId);
+        VibratorManagerService service = createSystemReadyService();
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS,
+                /* callback= */ null, vibratorId);
+        mTestLooper.dispatchAll();
+
+        assertThat(session).isNull();
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutVibratorIds_doesNotStart() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        VibratorManagerService service = createSystemReadyService();
+
+        int[] nullIds = null;
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, nullIds);
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+
+        int[] emptyIds = {};
+        session = startSession(service, RINGTONE_ATTRS, callback, emptyIds);
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+        verify(callback, never()).onFinishing();
+        verify(callback, times(2))
+                .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_badVibratorId_failsToStart() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        when(mNativeWrapperMock.startSession(anyLong(), any(int[].class))).thenReturn(false);
+        doReturn(false).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 3}));
+        doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 2}));
+        VibratorManagerService service = createSystemReadyService();
+
+        IBinder token = mock(IBinder.class);
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        doReturn(token).when(callback).asBinder();
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 3);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 3}));
+        verify(callback, never()).onStarted(any(IVibrationSession.class));
+        verify(callback, never()).onFinishing();
+        verify(callback)
+                .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenFinish_returnsSuccessAfterCallback() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        int sessionFinishDelayMs = 200;
+        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        captor.getValue().finishSession();
+
+        // Session not ended until HAL callback.
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+
+        // Dispatch HAL callbacks.
+        mTestLooper.moveTimeForward(sessionFinishDelayMs);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenSendCancelSignal_cancelsSession() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        int sessionFinishDelayMs = 200;
+        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        session.getCancellationSignal().cancel();
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenCancel_returnsCancelStatus() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        // Delay not applied when session is aborted.
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        captor.getValue().cancelSession();
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_finishThenCancel_returnsRightAwayWithFinishedStatus()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        // Delay not applied when session is aborted.
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        captor.getValue().finishSession();
+        mTestLooper.dispatchAll();
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+
+        captor.getValue().cancelSession();
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenHalCancels_returnsCancelStatus()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor =
+                ArgumentCaptor.forClass(
+                        VibratorManagerService.VibratorManagerNativeCallbacks.class);
+        verify(mNativeWrapperMock).init(listenerCaptor.capture());
+        doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class));
+
+        IBinder token = mock(IBinder.class);
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        doReturn(token).when(callback).asBinder();
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        // Mock HAL ending session unexpectedly.
+        listenerCaptor.getValue().onVibrationSessionComplete(session.getSessionId());
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_UNKNOWN_REASON);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withPowerMode_usesPowerModeState() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+        VendorVibrationSession session1 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        VendorVibrationSession session2 = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+        captor.getValue().cancelSession();
+        mTestLooper.dispatchAll();
+
+        mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
+        VendorVibrationSession session3 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never())
+                .startSession(eq(session1.getSessionId()), any(int[].class));
+        verify(mNativeWrapperMock).startSession(eq(session2.getSessionId()), eq(new int[] {1}));
+        verify(mNativeWrapperMock).startSession(eq(session3.getSessionId()), eq(new int[] {1}));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withOngoingHigherImportanceVibration_ignoresSession()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{10, 10_000}, new int[]{128, 255}, -1);
+        vibrate(service, effect, ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async.
+        // Wait until second step started to ensure the noteVibratorOn was triggered.
+        assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2,
+                service, TEST_TIMEOUT_MILLIS));
+
+        VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never())
+                .startSession(eq(session.getSessionId()), any(int[].class));
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE);
+        verify(callback, never()).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_IGNORED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withOngoingLowerImportanceVibration_cancelsOngoing()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{10, 10_000}, new int[]{128, 255}, -1);
+        HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+
+        // VibrationThread will start this vibration async.
+        // Wait until second step started to ensure the noteVibratorOn was triggered.
+        assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2, service,
+                TEST_TIMEOUT_MILLIS));
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        vibration.waitForEnd();
+        assertTrue(waitUntil(s -> session.isStarted(), service, TEST_TIMEOUT_MILLIS));
+        mTestLooper.dispatchAll();
+
+        assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 }));
+        verify(callback).onStarted(any(IVibrationSession.class));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withOngoingLowerImportanceExternalVibration_cancelsOngoing()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        IBinder firstToken = mock(IBinder.class);
+        IExternalVibrationController controller = mock(IExternalVibrationController.class);
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                AUDIO_ALARM_ATTRS,
+                controller, firstToken);
+        ExternalVibrationScale scale =
+                mExternalVibratorService.onExternalVibrationStart(externalVibration);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+        // The external vibration should have been cancelled
+        verify(controller).mute();
+        assertEquals(Arrays.asList(false, true, false),
+                mVibratorProviders.get(1).getExternalControlStates());
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 }));
+        verify(callback).onStarted(any(IVibrationSession.class));
+    }
+
+    @Test
     public void frameworkStats_externalVibration_reportsAllMetrics() throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
@@ -3050,6 +3547,30 @@
         when(mNativeWrapperMock.getVibratorIds()).thenReturn(vibratorIds);
     }
 
+    private IVibrationSessionCallback mockSessionCallbacks(long delayToEndSessionMillis) {
+        Handler handler = new Handler(mTestLooper.getLooper());
+        ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor =
+                ArgumentCaptor.forClass(
+                        VibratorManagerService.VibratorManagerNativeCallbacks.class);
+        verify(mNativeWrapperMock).init(listenerCaptor.capture());
+        doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class));
+        doAnswer(args -> {
+            handler.postDelayed(
+                    () -> listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0)),
+                    delayToEndSessionMillis);
+            return null;
+        }).when(mNativeWrapperMock).endSession(anyLong(), eq(false));
+        doAnswer(args -> {
+            listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0));
+            return null;
+        }).when(mNativeWrapperMock).endSession(anyLong(), eq(true));
+
+        IBinder token = mock(IBinder.class);
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        doReturn(token).when(callback).asBinder();
+        return callback;
+    }
+
     private void cancelVibrate(VibratorManagerService service) {
         service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service);
     }
@@ -3157,6 +3678,16 @@
         return vib;
     }
 
+    private VendorVibrationSession startSession(VibratorManagerService service,
+            VibrationAttributes attrs, IVibrationSessionCallback callback, int... vibratorIds) {
+        VendorVibrationSession session = service.startVendorVibrationSessionInternal(UID,
+                Context.DEVICE_ID_DEFAULT, PACKAGE_NAME, vibratorIds, attrs, "reason", callback);
+        if (session != null) {
+            mPendingSessions.add(session);
+        }
+        return session;
+    }
+
     private boolean waitUntil(Predicate<VibratorManagerService> predicate,
             VibratorManagerService service, long timeout) throws InterruptedException {
         long timeoutTimestamp = SystemClock.uptimeMillis() + timeout;
diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
index 07b7338..0da4521 100644
--- a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
+++ b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
@@ -143,6 +143,38 @@
     }
 
     @Test
+    public void testStartVendorVibrationSessionWithoutVibratePermissionFails() throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+                Manifest.permission.START_VIBRATION_SESSIONS);
+        expectSecurityException("VIBRATE");
+        mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME,
+                new int[] { 1 }, ATTRS, "testVibrate", null);
+    }
+
+    @Test
+    public void testStartVendorVibrationSessionWithoutVibrateVendorEffectsPermissionFails()
+            throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.VIBRATE,
+                Manifest.permission.START_VIBRATION_SESSIONS);
+        expectSecurityException("VIBRATE");
+        mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME,
+                new int[] { 1 }, ATTRS, "testVibrate", null);
+    }
+
+    @Test
+    public void testStartVendorVibrationSessionWithoutStartSessionPermissionFails()
+            throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.VIBRATE,
+                Manifest.permission.VIBRATE_VENDOR_EFFECTS);
+        expectSecurityException("VIBRATE");
+        mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME,
+                new int[] { 1 }, ATTRS, "testVibrate", null);
+    }
+
+    @Test
     public void testCancelVibrateFails() throws RemoteException {
         expectSecurityException("VIBRATE");
         mVibratorService.cancelVibrate(/* usageFilter= */ -1, new Binder());