Merge "Add device logger for active device and action state change" into main
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 10ce3b8..774ba74 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -1565,6 +1565,7 @@
     for (size_t i=0 ; i<pcount ; i++) {
         const Animation::Part& part(animation.parts[i]);
         const size_t fcount = part.frames.size();
+        glBindTexture(GL_TEXTURE_2D, 0);
 
         // Handle animation package
         if (part.animation != nullptr) {
@@ -1641,8 +1642,10 @@
                 if (r > 0) {
                     glBindTexture(GL_TEXTURE_2D, frame.tid);
                 } else {
-                    glGenTextures(1, &frame.tid);
-                    glBindTexture(GL_TEXTURE_2D, frame.tid);
+                    if (part.count != 1) {
+                        glGenTextures(1, &frame.tid);
+                        glBindTexture(GL_TEXTURE_2D, frame.tid);
+                    }
                     int w, h;
                     // Set decoding option to alpha unpremultiplied so that the R, G, B channels
                     // of transparent pixels are preserved.
diff --git a/core/api/current.txt b/core/api/current.txt
index 9ba84c9..a3d8978 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -15651,6 +15651,7 @@
 
   public final class Gainmap implements android.os.Parcelable {
     ctor public Gainmap(@NonNull android.graphics.Bitmap);
+    ctor public Gainmap(@NonNull android.graphics.Gainmap, @NonNull android.graphics.Bitmap);
     method public int describeContents();
     method @NonNull public float getDisplayRatioForFullHdr();
     method @NonNull public float[] getEpsilonHdr();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 7d2ef4d..6cad578 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1628,6 +1628,14 @@
      */
     public static final int GROUP_ALERT_CHILDREN = 2;
 
+    /**
+     * Constant for the {@link Builder#setGroup(String) group key} that is added to notifications
+     * that are not already grouped when {@link Builder#setSilent()} is used.
+     *
+     * @hide
+     */
+    public static final String GROUP_KEY_SILENT = "silent";
+
     private int mGroupAlertBehavior = GROUP_ALERT_ALL;
 
     /**
@@ -4290,6 +4298,35 @@
         }
 
         /**
+         * If {@code true}, silences this instance of the notification, regardless of the sounds or
+         * vibrations set on the notification or notification channel. If {@code false}, then the
+         * normal sound and vibration logic applies.
+         *
+         * @hide
+         */
+        public @NonNull Builder setSilent(boolean silent) {
+            if (!silent) {
+                return this;
+            }
+            if (mN.isGroupSummary()) {
+                setGroupAlertBehavior(GROUP_ALERT_CHILDREN);
+            } else {
+                setGroupAlertBehavior(GROUP_ALERT_SUMMARY);
+            }
+
+            setVibrate(null);
+            setSound(null);
+            mN.defaults &= ~DEFAULT_SOUND;
+            mN.defaults &= ~DEFAULT_VIBRATE;
+            setDefaults(mN.defaults);
+
+            if (TextUtils.isEmpty(mN.mGroupKey)) {
+                setGroup(GROUP_KEY_SILENT);
+            }
+            return this;
+        }
+
+        /**
          * Set the first line of text in the platform notification template.
          */
         @NonNull
diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
index ee7836f..ed8484f 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
@@ -21,6 +21,7 @@
 import android.companion.virtual.IVirtualDeviceSoundEffectListener;
 import android.companion.virtual.VirtualDevice;
 import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
 import android.hardware.display.IVirtualDisplayCallback;
 import android.hardware.display.VirtualDisplayConfig;
 
@@ -46,7 +47,7 @@
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     IVirtualDevice createVirtualDevice(
-            in IBinder token, String packageName, int associationId,
+            in IBinder token, in AttributionSource attributionSource, int associationId,
             in VirtualDeviceParams params, in IVirtualDeviceActivityListener activityListener,
             in IVirtualDeviceSoundEffectListener soundEffectListener);
 
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index f68cfff..d13bfd4 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -145,7 +145,7 @@
         mContext = context.getApplicationContext();
         mVirtualDevice = service.createVirtualDevice(
                 new Binder(),
-                mContext.getPackageName(),
+                mContext.getAttributionSource(),
                 associationId,
                 params,
                 mActivityListenerBinder,
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 45d6dc6..b6d8375 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -43,6 +43,7 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 
+import java.io.PrintWriter;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -501,6 +502,26 @@
                 + ")";
     }
 
+    /**
+     * Dumps debugging information about the VirtualDeviceParams
+     * @hide
+     */
+    public void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "mName=" + mName);
+        pw.println(prefix + "mLockState=" + mLockState);
+        pw.println(prefix + "mUsersWithMatchingAccounts=" + mUsersWithMatchingAccounts);
+        pw.println(prefix + "mAllowedCrossTaskNavigations=" + mAllowedCrossTaskNavigations);
+        pw.println(prefix + "mBlockedCrossTaskNavigations=" + mBlockedCrossTaskNavigations);
+        pw.println(prefix + "mAllowedActivities=" + mAllowedActivities);
+        pw.println(prefix + "mBlockedActivities=" + mBlockedActivities);
+        pw.println(prefix + "mDevicePolicies=" + mDevicePolicies);
+        pw.println(prefix + "mDefaultNavigationPolicy=" + mDefaultNavigationPolicy);
+        pw.println(prefix + "mDefaultActivityPolicy=" + mDefaultActivityPolicy);
+        pw.println(prefix + "mVirtualSensorConfigs=" + mVirtualSensorConfigs);
+        pw.println(prefix + "mAudioPlaybackSessionId=" + mAudioPlaybackSessionId);
+        pw.println(prefix + "mAudioRecordingSessionId=" + mAudioRecordingSessionId);
+    }
+
     @NonNull
     public static final Parcelable.Creator<VirtualDeviceParams> CREATOR =
             new Parcelable.Creator<VirtualDeviceParams>() {
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
index 3bdf9aa..0dbe411 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
@@ -104,6 +104,11 @@
         parcel.writeInt(mFlags);
     }
 
+    @Override
+    public String toString() {
+        return "VirtualSensorConfig{" + "mType=" + mType + ", mName='" + mName + '\'' + '}';
+    }
+
     /**
      * Returns the type of the sensor.
      *
diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java
index c3df17d..529363f 100644
--- a/core/java/android/content/pm/CrossProfileApps.java
+++ b/core/java/android/content/pm/CrossProfileApps.java
@@ -344,15 +344,22 @@
         // If there is a label for the launcher intent, then use that as it is typically shorter.
         // Otherwise, just use the top-level application name.
         Intent launchIntent = pm.getLaunchIntentForPackage(mContext.getPackageName());
+        if (launchIntent == null) {
+            return getDefaultCallingApplicationLabel();
+        }
         List<ResolveInfo> infos =
                 pm.queryIntentActivities(
                         launchIntent, PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY));
         if (infos.size() > 0) {
             return infos.get(0).loadLabel(pm);
         }
+        return getDefaultCallingApplicationLabel();
+    }
+
+    private CharSequence getDefaultCallingApplicationLabel() {
         return mContext.getApplicationInfo()
                 .loadSafeLabel(
-                        pm,
+                        mContext.getPackageManager(),
                         /* ellipsizeDip= */ 0,
                         TextUtils.SAFE_STRING_FLAG_SINGLE_LINE
                                 | TextUtils.SAFE_STRING_FLAG_TRIM);
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index c5585af..ea0f5ff 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -304,6 +304,8 @@
 
     boolean isPackageSuspendedForUser(String packageName, int userId);
 
+    boolean isPackageQuarantinedForUser(String packageName, int userId);
+
     Bundle getSuspendedPackageAppExtras(String packageName, int userId);
 
     /**
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index c2a0062..eedb25b 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -123,7 +123,7 @@
      * credential, display a picker when multiple credentials exist, etc.
      * Callers (e.g. browsers) may optionally set origin in {@link GetCredentialRequest} for an
      * app different from their own, to be able to get credentials on behalf of that app. They would
-     * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN}
+     * need additional permission {@code CREDENTIAL_MANAGER_SET_ORIGIN}
      * to use this functionality
      *
      * @param context the context used to launch any UI needed; use an activity context to make sure
@@ -209,9 +209,9 @@
      *
      * <p>This API doesn't invoke any UI. It only performs the preparation work so that you can
      * later launch the remaining get-credential operation (involves UIs) through the {@link
-     * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Context,
+     * #getCredential(Context, PrepareGetCredentialResponse.PendingGetCredentialHandle,
      * CancellationSignal, Executor, OutcomeReceiver)} API which incurs less latency compared to
-     * the {@link #getCredential(GetCredentialRequest, Context, CancellationSignal, Executor,
+     * the {@link #getCredential(Context, GetCredentialRequest, CancellationSignal, Executor,
      * OutcomeReceiver)} API that executes the whole operation in one call.
      *
      * @param request            the request specifying type(s) of credentials to get from the user
@@ -261,7 +261,7 @@
      * storing the new credential, etc.
      * Callers (e.g. browsers) may optionally set origin in {@link CreateCredentialRequest} for an
      * app different from their own, to be able to get credentials on behalf of that app. They would
-     * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN}
+     * need additional permission {@code CREDENTIAL_MANAGER_SET_ORIGIN}
      * to use this functionality
      *
      * @param context the context used to launch any UI needed; use an activity context to make sure
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 912e8df..af448f0 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -466,6 +466,19 @@
         // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/PromptInfo.java)
 
         /**
+         * Set if emergency call button should show, for example if biometrics are
+         * required to access the dialer app
+         * @param showEmergencyCallButton if true, shows emergency call button
+         * @return This builder.
+         * @hide
+         */
+        @NonNull
+        public Builder setShowEmergencyCallButton(boolean showEmergencyCallButton) {
+            mPromptInfo.setShowEmergencyCallButton(showEmergencyCallButton);
+            return this;
+        }
+
+        /**
          * Creates a {@link BiometricPrompt}.
          *
          * @return An instance of {@link BiometricPrompt}.
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index e275078..24cfd164 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -48,6 +48,7 @@
     private boolean mAllowBackgroundAuthentication;
     private boolean mIgnoreEnrollmentState;
     private boolean mIsForLegacyFingerprintManager = false;
+    private boolean mShowEmergencyCallButton = false;
 
     public PromptInfo() {
 
@@ -72,6 +73,7 @@
         mAllowBackgroundAuthentication = in.readBoolean();
         mIgnoreEnrollmentState = in.readBoolean();
         mIsForLegacyFingerprintManager = in.readBoolean();
+        mShowEmergencyCallButton = in.readBoolean();
     }
 
     public static final Creator<PromptInfo> CREATOR = new Creator<PromptInfo>() {
@@ -111,6 +113,7 @@
         dest.writeBoolean(mAllowBackgroundAuthentication);
         dest.writeBoolean(mIgnoreEnrollmentState);
         dest.writeBoolean(mIsForLegacyFingerprintManager);
+        dest.writeBoolean(mShowEmergencyCallButton);
     }
 
     // LINT.IfChange
@@ -228,6 +231,10 @@
         mAllowedSensorIds.add(sensorId);
     }
 
+    public void setShowEmergencyCallButton(boolean showEmergencyCallButton) {
+        mShowEmergencyCallButton = showEmergencyCallButton;
+    }
+
     // Getters
 
     public CharSequence getTitle() {
@@ -309,4 +316,8 @@
     public boolean isForLegacyFingerprintManager() {
         return mIsForLegacyFingerprintManager;
     }
+
+    public boolean isShowEmergencyCallButton() {
+        return mShowEmergencyCallButton;
+    }
 }
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 6baf91d7..ea951a5 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -236,9 +236,10 @@
         private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER =
                 new CameraExtensionManagerGlobal();
         private final Object mLock = new Object();
-        private final int PROXY_SERVICE_DELAY_MS = 1000;
+        private final int PROXY_SERVICE_DELAY_MS = 2000;
         private InitializerFuture mInitFuture = null;
         private ServiceConnection mConnection = null;
+        private int mConnectionCount = 0;
         private ICameraExtensionsProxyService mProxy = null;
         private boolean mSupportsAdvancedExtensions = false;
 
@@ -249,6 +250,15 @@
             return GLOBAL_CAMERA_MANAGER;
         }
 
+        private void releaseProxyConnectionLocked(Context ctx) {
+            if (mConnection != null ) {
+                ctx.unbindService(mConnection);
+                mConnection = null;
+                mProxy = null;
+                mConnectionCount = 0;
+            }
+        }
+
         private void connectToProxyLocked(Context ctx) {
             if (mConnection == null) {
                 Intent intent = new Intent();
@@ -270,7 +280,6 @@
                 mConnection = new ServiceConnection() {
                     @Override
                     public void onServiceDisconnected(ComponentName component) {
-                        mInitFuture.setStatus(false);
                         mConnection = null;
                         mProxy = null;
                     }
@@ -348,23 +357,32 @@
 
         public boolean registerClient(Context ctx, IBinder token) {
             synchronized (mLock) {
+                boolean ret = false;
                 connectToProxyLocked(ctx);
                 if (mProxy == null) {
                     return false;
                 }
+                mConnectionCount++;
 
                 try {
-                    return mProxy.registerClient(token);
+                    ret = mProxy.registerClient(token);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to initialize extension! Extension service does "
                             + " not respond!");
                 }
+                if (!ret) {
+                    mConnectionCount--;
+                }
 
-                return false;
+                if (mConnectionCount <= 0) {
+                    releaseProxyConnectionLocked(ctx);
+                }
+
+                return ret;
             }
         }
 
-        public void unregisterClient(IBinder token) {
+        public void unregisterClient(Context ctx, IBinder token) {
             synchronized (mLock) {
                 if (mProxy != null) {
                     try {
@@ -372,6 +390,11 @@
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to de-initialize extension! Extension service does"
                                 + " not respond!");
+                    } finally {
+                        mConnectionCount--;
+                        if (mConnectionCount <= 0) {
+                            releaseProxyConnectionLocked(ctx);
+                        }
                     }
                 }
             }
@@ -446,8 +469,8 @@
     /**
      * @hide
      */
-    public static void unregisterClient(IBinder token) {
-        CameraExtensionManagerGlobal.get().unregisterClient(token);
+    public static void unregisterClient(Context ctx, IBinder token) {
+        CameraExtensionManagerGlobal.get().unregisterClient(ctx, token);
     }
 
     /**
@@ -578,7 +601,7 @@
                 }
             }
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableList(ret);
@@ -626,7 +649,7 @@
             Log.e(TAG, "Failed to query the extension for postview availability! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return false;
@@ -722,7 +745,7 @@
                     + "service does not respond!");
             return Collections.emptyList();
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
     }
 
@@ -791,7 +814,7 @@
                     + " not respond!");
             return new ArrayList<>();
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
     }
 
@@ -872,7 +895,7 @@
                     }
                 }
             } finally {
-                unregisterClient(token);
+                unregisterClient(mContext, token);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to query the extension supported sizes! Extension service does"
@@ -957,7 +980,7 @@
             Log.e(TAG, "Failed to query the extension capture latency! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return null;
@@ -998,7 +1021,7 @@
             Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return false;
@@ -1075,7 +1098,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture request keys!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableSet(ret);
@@ -1155,7 +1178,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture result keys!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableSet(ret);
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index e06699b..c7e74c0 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -90,7 +90,7 @@
     private final HashMap<Integer, ImageReader> mReaderMap = new HashMap<>();
     private RequestProcessor mRequestProcessor = new RequestProcessor();
     private final int mSessionId;
-    private final IBinder mToken;
+    private IBinder mToken = null;
 
     private Surface mClientRepeatingRequestSurface;
     private Surface mClientCaptureSurface;
@@ -103,6 +103,8 @@
     private boolean mInitialized;
     private boolean mSessionClosed;
 
+    private final Context mContext;
+
     // Lock to synchronize cross-thread access to device public interface
     final Object mInterfaceLock;
 
@@ -113,14 +115,9 @@
     public static CameraAdvancedExtensionSessionImpl createCameraAdvancedExtensionSession(
             @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
             @NonNull Map<String, CameraCharacteristics> characteristicsMap,
-            @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId)
+            @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId,
+            @NonNull IBinder token)
             throws CameraAccessException, RemoteException {
-        final IBinder token = new Binder(TAG + " : " + sessionId);
-        boolean success = CameraExtensionCharacteristics.registerClient(ctx, token);
-        if (!success) {
-            throw new UnsupportedOperationException("Unsupported extension!");
-        }
-
         String cameraId = cameraDevice.getId();
         CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx,
                 cameraId, characteristicsMap);
@@ -204,8 +201,9 @@
         IAdvancedExtenderImpl extender = CameraExtensionCharacteristics.initializeAdvancedExtension(
                 config.getExtension());
         extender.init(cameraId, characteristicsMapNative);
-        CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(extender,
-                cameraDevice, characteristicsMapNative, repeatingRequestSurface,
+
+        CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx,
+                extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface,
                 burstCaptureSurface, postviewSurface, config.getStateCallback(),
                 config.getExecutor(), sessionId, token);
 
@@ -217,13 +215,16 @@
         return ret;
     }
 
-    private CameraAdvancedExtensionSessionImpl(@NonNull IAdvancedExtenderImpl extender,
+    private CameraAdvancedExtensionSessionImpl(Context ctx,
+            @NonNull IAdvancedExtenderImpl extender,
             @NonNull CameraDeviceImpl cameraDevice,
             Map<String, CameraMetadataNative> characteristicsMap,
             @Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface,
             @Nullable Surface postviewSurface,
             @NonNull StateCallback callback, @NonNull Executor executor,
-            int sessionId, @NonNull IBinder token) {
+            int sessionId,
+            @NonNull IBinder token) {
+        mContext = ctx;
         mAdvancedExtender = extender;
         mCameraDevice = cameraDevice;
         mCharacteristicsMap = characteristicsMap;
@@ -578,12 +579,16 @@
                 mSessionProcessor = null;
             }
 
-            CameraExtensionCharacteristics.unregisterClient(mToken);
-            if (mInitialized || (mCaptureSession != null)) {
-                notifyClose = true;
-                CameraExtensionCharacteristics.releaseSession();
+
+            if (mToken != null) {
+                if (mInitialized || (mCaptureSession != null)) {
+                    notifyClose = true;
+                    CameraExtensionCharacteristics.releaseSession();
+                }
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
             }
             mInitialized = false;
+            mToken = null;
 
             for (ImageReader reader : mReaderMap.values()) {
                 reader.close();
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index d3bde4b..181ab2c 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -2550,19 +2550,32 @@
         HashMap<String, CameraCharacteristics> characteristicsMap = new HashMap<>(
                 mPhysicalIdsToChars);
         characteristicsMap.put(mCameraId, mCharacteristics);
+        boolean initializationFailed = true;
+        IBinder token = new Binder(TAG + " : " + mNextSessionId++);
         try {
+            boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token);
+            if (!ret) {
+                token = null;
+                throw new UnsupportedOperationException("Unsupported extension!");
+            }
+
             if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) {
                 mCurrentAdvancedExtensionSession =
                         CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession(
                                 this, characteristicsMap, mContext, extensionConfiguration,
-                                mNextSessionId++);
+                                mNextSessionId, token);
             } else {
                 mCurrentExtensionSession = CameraExtensionSessionImpl.createCameraExtensionSession(
                         this, characteristicsMap, mContext, extensionConfiguration,
-                        mNextSessionId++);
+                        mNextSessionId, token);
             }
+            initializationFailed = false;
         } catch (RemoteException e) {
             throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
+        } finally {
+            if (initializationFailed && (token != null)) {
+                CameraExtensionCharacteristics.unregisterClient(mContext, token);
+            }
         }
     }
 }
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 5d25681..bf77681 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -91,7 +91,7 @@
     private final Set<CaptureRequest.Key> mSupportedRequestKeys;
     private final Set<CaptureResult.Key> mSupportedResultKeys;
     private final ExtensionSessionStatsAggregator mStatsAggregator;
-    private final IBinder mToken;
+    private IBinder mToken = null;
     private boolean mCaptureResultsSupported;
 
     private CameraCaptureSession mCaptureSession = null;
@@ -119,6 +119,8 @@
     // will do so internally.
     private boolean mInternalRepeatingRequestEnabled = true;
 
+    private final Context mContext;
+
     // Lock to synchronize cross-thread access to device public interface
     final Object mInterfaceLock;
 
@@ -135,14 +137,9 @@
             @NonNull Map<String, CameraCharacteristics> characteristicsMap,
             @NonNull Context ctx,
             @NonNull ExtensionSessionConfiguration config,
-            int sessionId)
+            int sessionId,
+            @NonNull IBinder token)
             throws CameraAccessException, RemoteException {
-        final IBinder token = new Binder(TAG + " : " + sessionId);
-        boolean success = CameraExtensionCharacteristics.registerClient(ctx, token);
-        if (!success) {
-            throw new UnsupportedOperationException("Unsupported extension!");
-        }
-
         String cameraId = cameraDevice.getId();
         CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx,
                 cameraId, characteristicsMap);
@@ -234,6 +231,7 @@
                 characteristicsMap.get(cameraId).getNativeMetadata());
 
         CameraExtensionSessionImpl session = new CameraExtensionSessionImpl(
+                ctx,
                 extenders.second,
                 extenders.first,
                 supportedPreviewSizes,
@@ -256,7 +254,7 @@
         return session;
     }
 
-    public CameraExtensionSessionImpl(@NonNull IImageCaptureExtenderImpl imageExtender,
+    public CameraExtensionSessionImpl(Context ctx, @NonNull IImageCaptureExtenderImpl imageExtender,
             @NonNull IPreviewExtenderImpl previewExtender,
             @NonNull List<Size> previewSizes,
             @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
@@ -269,6 +267,7 @@
             @NonNull IBinder token,
             @NonNull Set<CaptureRequest.Key> requestKeys,
             @Nullable Set<CaptureResult.Key> resultKeys) {
+        mContext = ctx;
         mImageExtender = imageExtender;
         mPreviewExtender = previewExtender;
         mCameraDevice = cameraDevice;
@@ -878,12 +877,15 @@
                         + " respond!");
             }
 
-            CameraExtensionCharacteristics.unregisterClient(mToken);
-            if (mInitialized || (mCaptureSession != null)) {
-                notifyClose = true;
-                CameraExtensionCharacteristics.releaseSession();
+            if (mToken != null) {
+                if (mInitialized || (mCaptureSession != null)) {
+                    notifyClose = true;
+                    CameraExtensionCharacteristics.releaseSession();
+                }
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
             }
             mInitialized = false;
+            mToken = null;
 
             if (mRepeatingRequestImageCallback != null) {
                 mRepeatingRequestImageCallback.close();
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index 21540bf..5e53373 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -1193,12 +1193,12 @@
         String physicalCameraId = source.readString();
         boolean isMultiResolutionOutput = source.readInt() == 1;
         int[] sensorPixelModesUsed = source.createIntArray();
-        long streamUseCase = source.readLong();
 
         checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
         long dynamicRangeProfile = source.readLong();
         DynamicRangeProfiles.checkProfileValue(dynamicRangeProfile);
         int colorSpace = source.readInt();
+        long streamUseCase = source.readLong();
 
         int timestampBase = source.readInt();
         int mirrorMode = source.readInt();
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 94bff89..4700720 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -370,8 +370,9 @@
 
     /**
      * Returns the default size of the surface associated with the display, or null if the surface
-     * is not provided for layer mirroring by SurfaceFlinger.
-     * Only used for mirroring started from MediaProjection.
+     * is not provided for layer mirroring by SurfaceFlinger. Size is rotated to reflect the current
+     * display device orientation.
+     * Used for mirroring from MediaProjection, or a physical display based on display flags.
      */
     public abstract Point getDisplaySurfaceDefaultSize(int displayId);
 
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 6ef1dc0..8f7725ec 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -10,6 +10,7 @@
 # BatteryStats
 per-file *BatteryConsumer* = file:/BATTERY_STATS_OWNERS
 per-file BatteryManager* = file:/BATTERY_STATS_OWNERS
+per-file PowerMonitor* = file:/BATTERY_STATS_OWNERS
 per-file PowerComponents.java = file:/BATTERY_STATS_OWNERS
 per-file *Stats* = file:/BATTERY_STATS_OWNERS
 
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index bf72b1d..2cda787 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -18,26 +18,19 @@
 
 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.IVibrator;
+import android.os.vibrator.VibratorInfoFactory;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.util.Range;
-import android.util.Slog;
 import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.function.Function;
 
 /**
  * Vibrator implementation that controls the main system vibrator.
@@ -82,7 +75,7 @@
             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.
-                return mVibratorInfo = new NoVibratorInfo();
+                return mVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO;
             }
             VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length];
             for (int i = 0; i < vibratorIds.length; i++) {
@@ -96,12 +89,7 @@
                 }
                 vibratorInfos[i] = vibrator.getInfo();
             }
-            if (vibratorInfos.length == 1) {
-                // Device has a single vibrator info, cache and return successfully loaded info.
-                return mVibratorInfo = new VibratorInfo(/* id= */ -1, vibratorInfos[0]);
-            }
-            // Device has multiple vibrators, generate a single info representing all of them.
-            return mVibratorInfo = new MultiVibratorInfo(vibratorInfos);
+            return mVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, vibratorInfos);
         }
     }
 
@@ -275,296 +263,6 @@
     }
 
     /**
-     * Represents a device with no vibrator as a single {@link VibratorInfo}.
-     *
-     * @hide
-     */
-    @VisibleForTesting
-    public static class NoVibratorInfo extends VibratorInfo {
-        public NoVibratorInfo() {
-            // Use empty arrays to indicate no support, while null would indicate support unknown.
-            super(/* id= */ -1,
-                    /* capabilities= */ 0,
-                    /* supportedEffects= */ new SparseBooleanArray(),
-                    /* supportedBraking= */ new SparseBooleanArray(),
-                    /* supportedPrimitives= */ new SparseIntArray(),
-                    /* primitiveDelayMax= */ 0,
-                    /* compositionSizeMax= */ 0,
-                    /* pwlePrimitiveDurationMax= */ 0,
-                    /* pwleSizeMax= */ 0,
-                    /* qFactor= */ Float.NaN,
-                    new FrequencyProfile(/* resonantFrequencyHz= */ Float.NaN,
-                            /* minFrequencyHz= */ Float.NaN,
-                            /* frequencyResolutionHz= */ Float.NaN,
-                            /* maxAmplitudes= */ null));
-        }
-    }
-
-    /**
-     * Represents multiple vibrator information as a single {@link VibratorInfo}.
-     *
-     * <p>This uses an intersection of all vibrators to decide the capabilities and effect/primitive
-     * support.
-     *
-     * @hide
-     */
-    @VisibleForTesting
-    public static class MultiVibratorInfo extends VibratorInfo {
-        // Epsilon used for float comparison applied in calculations for the merged info.
-        private static final float EPSILON = 1e-5f;
-
-        public MultiVibratorInfo(VibratorInfo[] vibrators) {
-            // Need to use an extra constructor to share the computation in super initialization.
-            this(vibrators, frequencyProfileIntersection(vibrators));
-        }
-
-        private MultiVibratorInfo(VibratorInfo[] vibrators,
-                VibratorInfo.FrequencyProfile mergedProfile) {
-            super(/* id= */ -1,
-                    capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
-                    supportedEffectsIntersection(vibrators),
-                    supportedBrakingIntersection(vibrators),
-                    supportedPrimitivesAndDurationsIntersection(vibrators),
-                    integerLimitIntersection(vibrators, VibratorInfo::getPrimitiveDelayMax),
-                    integerLimitIntersection(vibrators, VibratorInfo::getCompositionSizeMax),
-                    integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
-                    integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
-                    floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
-                    mergedProfile);
-        }
-
-        private static int capabilitiesIntersection(VibratorInfo[] infos,
-                boolean frequencyProfileIsEmpty) {
-            int intersection = ~0;
-            for (VibratorInfo info : infos) {
-                intersection &= info.getCapabilities();
-            }
-            if (frequencyProfileIsEmpty) {
-                // Revoke frequency control if the merged frequency profile ended up empty.
-                intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
-            }
-            return intersection;
-        }
-
-        @Nullable
-        private static SparseBooleanArray supportedBrakingIntersection(VibratorInfo[] infos) {
-            for (VibratorInfo info : infos) {
-                if (!info.isBrakingSupportKnown()) {
-                    // If one vibrator support is unknown, then the intersection is also unknown.
-                    return null;
-                }
-            }
-
-            SparseBooleanArray intersection = new SparseBooleanArray();
-            SparseBooleanArray firstVibratorBraking = infos[0].getSupportedBraking();
-
-            brakingIdLoop:
-            for (int i = 0; i < firstVibratorBraking.size(); i++) {
-                int brakingId = firstVibratorBraking.keyAt(i);
-                if (!firstVibratorBraking.valueAt(i)) {
-                    // The first vibrator already doesn't support this braking, so skip it.
-                    continue brakingIdLoop;
-                }
-
-                for (int j = 1; j < infos.length; j++) {
-                    if (!infos[j].hasBrakingSupport(brakingId)) {
-                        // One vibrator doesn't support this braking, so the intersection doesn't.
-                        continue brakingIdLoop;
-                    }
-                }
-
-                intersection.put(brakingId, true);
-            }
-
-            return intersection;
-        }
-
-        @Nullable
-        private static SparseBooleanArray supportedEffectsIntersection(VibratorInfo[] infos) {
-            for (VibratorInfo info : infos) {
-                if (!info.isEffectSupportKnown()) {
-                    // If one vibrator support is unknown, then the intersection is also unknown.
-                    return null;
-                }
-            }
-
-            SparseBooleanArray intersection = new SparseBooleanArray();
-            SparseBooleanArray firstVibratorEffects = infos[0].getSupportedEffects();
-
-            effectIdLoop:
-            for (int i = 0; i < firstVibratorEffects.size(); i++) {
-                int effectId = firstVibratorEffects.keyAt(i);
-                if (!firstVibratorEffects.valueAt(i)) {
-                    // The first vibrator already doesn't support this effect, so skip it.
-                    continue effectIdLoop;
-                }
-
-                for (int j = 1; j < infos.length; j++) {
-                    if (infos[j].isEffectSupported(effectId) != VIBRATION_EFFECT_SUPPORT_YES) {
-                        // One vibrator doesn't support this effect, so the intersection doesn't.
-                        continue effectIdLoop;
-                    }
-                }
-
-                intersection.put(effectId, true);
-            }
-
-            return intersection;
-        }
-
-        @NonNull
-        private static SparseIntArray supportedPrimitivesAndDurationsIntersection(
-                VibratorInfo[] infos) {
-            SparseIntArray intersection = new SparseIntArray();
-            SparseIntArray firstVibratorPrimitives = infos[0].getSupportedPrimitives();
-
-            primitiveIdLoop:
-            for (int i = 0; i < firstVibratorPrimitives.size(); i++) {
-                int primitiveId = firstVibratorPrimitives.keyAt(i);
-                int primitiveDuration = firstVibratorPrimitives.valueAt(i);
-                if (primitiveDuration == 0) {
-                    // The first vibrator already doesn't support this primitive, so skip it.
-                    continue primitiveIdLoop;
-                }
-
-                for (int j = 1; j < infos.length; j++) {
-                    int vibratorPrimitiveDuration = infos[j].getPrimitiveDuration(primitiveId);
-                    if (vibratorPrimitiveDuration == 0) {
-                        // One vibrator doesn't support this primitive, so the intersection doesn't.
-                        continue primitiveIdLoop;
-                    } else {
-                        // The primitive vibration duration is the maximum among all vibrators.
-                        primitiveDuration = Math.max(primitiveDuration, vibratorPrimitiveDuration);
-                    }
-                }
-
-                intersection.put(primitiveId, primitiveDuration);
-            }
-            return intersection;
-        }
-
-        private static int integerLimitIntersection(VibratorInfo[] infos,
-                Function<VibratorInfo, Integer> propertyGetter) {
-            int limit = 0; // Limit 0 means unlimited
-            for (VibratorInfo info : infos) {
-                int vibratorLimit = propertyGetter.apply(info);
-                if ((limit == 0) || (vibratorLimit > 0 && vibratorLimit < limit)) {
-                    // This vibrator is limited and intersection is unlimited or has a larger limit:
-                    // use smaller limit here for the intersection.
-                    limit = vibratorLimit;
-                }
-            }
-            return limit;
-        }
-
-        private static float floatPropertyIntersection(VibratorInfo[] infos,
-                Function<VibratorInfo, Float> propertyGetter) {
-            float property = propertyGetter.apply(infos[0]);
-            if (Float.isNaN(property)) {
-                // If one vibrator is undefined then the intersection is undefined.
-                return Float.NaN;
-            }
-            for (int i = 1; i < infos.length; i++) {
-                if (Float.compare(property, propertyGetter.apply(infos[i])) != 0) {
-                    // If one vibrator has a different value then the intersection is undefined.
-                    return Float.NaN;
-                }
-            }
-            return property;
-        }
-
-        @NonNull
-        private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) {
-            float freqResolution = floatPropertyIntersection(infos,
-                    info -> info.getFrequencyProfile().getFrequencyResolutionHz());
-            float resonantFreq = floatPropertyIntersection(infos,
-                    VibratorInfo::getResonantFrequencyHz);
-            Range<Float> freqRange = frequencyRangeIntersection(infos, freqResolution);
-
-            if ((freqRange == null) || Float.isNaN(freqResolution)) {
-                return new FrequencyProfile(resonantFreq, Float.NaN, freqResolution, null);
-            }
-
-            int amplitudeCount =
-                    Math.round(1 + (freqRange.getUpper() - freqRange.getLower()) / freqResolution);
-            float[] maxAmplitudes = new float[amplitudeCount];
-
-            // Use MAX_VALUE here to ensure that the FrequencyProfile constructor called with this
-            // will fail if the loop below is broken and do not replace filled values with actual
-            // vibrator measurements.
-            Arrays.fill(maxAmplitudes, Float.MAX_VALUE);
-
-            for (VibratorInfo info : infos) {
-                Range<Float> vibratorFreqRange = info.getFrequencyProfile().getFrequencyRangeHz();
-                float[] vibratorMaxAmplitudes = info.getFrequencyProfile().getMaxAmplitudes();
-                int vibratorStartIdx = Math.round(
-                        (freqRange.getLower() - vibratorFreqRange.getLower()) / freqResolution);
-                int vibratorEndIdx = vibratorStartIdx + maxAmplitudes.length - 1;
-
-                if ((vibratorStartIdx < 0) || (vibratorEndIdx >= vibratorMaxAmplitudes.length)) {
-                    Slog.w(TAG, "Error calculating the intersection of vibrator frequency"
-                            + " profiles: attempted to fetch from vibrator "
-                            + info.getId() + " max amplitude with bad index " + vibratorStartIdx);
-                    return new FrequencyProfile(resonantFreq, Float.NaN, Float.NaN, null);
-                }
-
-                for (int i = 0; i < maxAmplitudes.length; i++) {
-                    maxAmplitudes[i] = Math.min(maxAmplitudes[i],
-                            vibratorMaxAmplitudes[vibratorStartIdx + i]);
-                }
-            }
-
-            return new FrequencyProfile(resonantFreq, freqRange.getLower(),
-                    freqResolution, maxAmplitudes);
-        }
-
-        @Nullable
-        private static Range<Float> frequencyRangeIntersection(VibratorInfo[] infos,
-                float frequencyResolution) {
-            Range<Float> firstRange = infos[0].getFrequencyProfile().getFrequencyRangeHz();
-            if (firstRange == null) {
-                // If one vibrator is undefined then the intersection is undefined.
-                return null;
-            }
-            float intersectionLower = firstRange.getLower();
-            float intersectionUpper = firstRange.getUpper();
-
-            // Generate the intersection of all vibrator supported ranges, making sure that both
-            // min supported frequencies are aligned w.r.t. the frequency resolution.
-
-            for (int i = 1; i < infos.length; i++) {
-                Range<Float> vibratorRange = infos[i].getFrequencyProfile().getFrequencyRangeHz();
-                if (vibratorRange == null) {
-                    // If one vibrator is undefined then the intersection is undefined.
-                    return null;
-                }
-
-                if ((vibratorRange.getLower() >= intersectionUpper)
-                        || (vibratorRange.getUpper() <= intersectionLower)) {
-                    // If the range and intersection are disjoint then the intersection is undefined
-                    return null;
-                }
-
-                float frequencyDelta = Math.abs(intersectionLower - vibratorRange.getLower());
-                if ((frequencyDelta % frequencyResolution) > EPSILON) {
-                    // If the intersection is not aligned with one vibrator then it's undefined
-                    return null;
-                }
-
-                intersectionLower = Math.max(intersectionLower, vibratorRange.getLower());
-                intersectionUpper = Math.min(intersectionUpper, vibratorRange.getUpper());
-            }
-
-            if ((intersectionUpper - intersectionLower) < frequencyResolution) {
-                // If the intersection is empty then it's undefined.
-                return null;
-            }
-
-            return Range.create(intersectionLower, intersectionUpper);
-        }
-    }
-
-    /**
      * Listener for all vibrators state change.
      *
      * <p>This registers a listener to all vibrators to merge the callbacks into a single state
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index b24b45d..08b32bf 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -525,14 +525,14 @@
     public abstract long getDuration();
 
     /**
-     * Checks if a given {@link Vibrator} can play this effect as intended.
+     * Checks if a vibrator with a given {@link VibratorInfo} can play this effect as intended.
      *
-     * <p>See @link Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more information
-     * about what counts as supported by a vibrator, and what counts as not.
+     * <p>See {@link VibratorInfo#areVibrationFeaturesSupported(VibrationEffect)} for more
+     * information about what counts as supported by a vibrator, and what counts as not.
      *
      * @hide
      */
-    public abstract boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator);
+    public abstract boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo);
 
     /**
      * Returns true if this effect could represent a touch haptic feedback.
@@ -813,9 +813,9 @@
 
         /** @hide */
         @Override
-        public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) {
+        public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
             for (VibrationEffectSegment segment : mSegments) {
-                if (!segment.areVibrationFeaturesSupported(vibrator)) {
+                if (!segment.areVibrationFeaturesSupported(vibratorInfo)) {
                     return false;
                 }
             }
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index 4e852e3..79e0ca8 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -216,9 +216,7 @@
      */
     @TestApi
     public boolean hasFrequencyControl() {
-        // We currently can only control frequency of the vibration using the compose PWLE method.
-        return getInfo().hasCapability(
-                IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
+        return getInfo().hasFrequencyControl();
     }
 
     /**
@@ -240,7 +238,7 @@
      * @hide
      */
     public boolean areVibrationFeaturesSupported(@NonNull VibrationEffect effect) {
-        return effect.areVibrationFeaturesSupported(this);
+        return getInfo().areVibrationFeaturesSupported(effect);
     }
 
     /**
diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java
index 02e6856..4f8c24d 100644
--- a/core/java/android/os/VibratorInfo.java
+++ b/core/java/android/os/VibratorInfo.java
@@ -156,6 +156,16 @@
             return false;
         }
         VibratorInfo that = (VibratorInfo) o;
+        return mId == that.mId && equalContent(that);
+    }
+
+    /**
+     * Returns {@code true} only if the properties and capabilities of the provided info, except for
+     * the ID, equals to this info. Returns {@code false} otherwise.
+     *
+     * @hide
+     */
+    public boolean equalContent(VibratorInfo that) {
         int supportedPrimitivesCount = mSupportedPrimitives.size();
         if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) {
             return false;
@@ -168,7 +178,7 @@
                 return false;
             }
         }
-        return mId == that.mId && mCapabilities == that.mCapabilities
+        return mCapabilities == that.mCapabilities
                 && mPrimitiveDelayMax == that.mPrimitiveDelayMax
                 && mCompositionSizeMax == that.mCompositionSizeMax
                 && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax
@@ -242,6 +252,17 @@
     }
 
     /**
+     * Check whether the vibrator has frequency control.
+     *
+     * @return True if the hardware can control the frequency of the vibrations, otherwise false.
+     */
+    public boolean hasFrequencyControl() {
+        // We currently can only control frequency of the vibration using the compose PWLE method.
+        return hasCapability(
+                IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
+    }
+
+    /**
      * Returns a default value to be applied to composed PWLE effects for braking.
      *
      * @return a supported braking value, one of android.hardware.vibrator.Braking.*
@@ -323,6 +344,23 @@
     }
 
     /**
+     * Query whether or not the vibrator supports all components of a given {@link VibrationEffect}
+     * (i.e. the vibrator can play the given effect as intended).
+     *
+     * <p>See {@link Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more
+     * information on how the vibrator support is determined.
+     *
+     * @param effect the {@link VibrationEffect} to check if it is supported
+     * @return {@code true} if the vibrator can play the given {@code effect} as intended,
+     *         {@code false} otherwise.
+     *
+     * @hide
+     */
+    public boolean areVibrationFeaturesSupported(@NonNull VibrationEffect effect) {
+        return effect.areVibrationFeaturesSupported(this);
+    }
+
+    /**
      * Query the estimated duration of given primitive.
      *
      * @param primitiveId Which primitives to query for.
@@ -417,7 +455,8 @@
         return mFrequencyProfile;
     }
 
-    protected long getCapabilities() {
+    /** Returns a single int representing all the capabilities of the vibrator. */
+    public long getCapabilities() {
         return mCapabilities;
     }
 
diff --git a/core/java/android/os/vibrator/MultiVibratorInfo.java b/core/java/android/os/vibrator/MultiVibratorInfo.java
new file mode 100644
index 0000000..5f32731
--- /dev/null
+++ b/core/java/android/os/vibrator/MultiVibratorInfo.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2023 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.vibrator.IVibrator;
+import android.os.Vibrator;
+import android.os.VibratorInfo;
+import android.util.Range;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import java.util.Arrays;
+import java.util.function.Function;
+
+/**
+ * Represents multiple vibrator information as a single {@link VibratorInfo}.
+ *
+ * <p>This uses an intersection of all vibrators to decide the capabilities and effect/primitive
+ * support.
+ *
+ * @hide
+ */
+public final class MultiVibratorInfo extends VibratorInfo {
+    private static final String TAG = "MultiVibratorInfo";
+
+    // Epsilon used for float comparison applied in calculations for the merged info.
+    private static final float EPSILON = 1e-5f;
+
+    public MultiVibratorInfo(int id, VibratorInfo[] vibrators) {
+        this(id, vibrators, frequencyProfileIntersection(vibrators));
+    }
+
+    private MultiVibratorInfo(
+            int id, VibratorInfo[] vibrators, VibratorInfo.FrequencyProfile mergedProfile) {
+        super(id,
+                capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
+                supportedEffectsIntersection(vibrators),
+                supportedBrakingIntersection(vibrators),
+                supportedPrimitivesAndDurationsIntersection(vibrators),
+                integerLimitIntersection(vibrators, VibratorInfo::getPrimitiveDelayMax),
+                integerLimitIntersection(vibrators, VibratorInfo::getCompositionSizeMax),
+                integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
+                integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
+                floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
+                mergedProfile);
+    }
+
+    private static int capabilitiesIntersection(VibratorInfo[] infos,
+            boolean frequencyProfileIsEmpty) {
+        int intersection = ~0;
+        for (VibratorInfo info : infos) {
+            intersection &= info.getCapabilities();
+        }
+        if (frequencyProfileIsEmpty) {
+            // Revoke frequency control if the merged frequency profile ended up empty.
+            intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
+        }
+        return intersection;
+    }
+
+    @Nullable
+    private static SparseBooleanArray supportedBrakingIntersection(VibratorInfo[] infos) {
+        for (VibratorInfo info : infos) {
+            if (!info.isBrakingSupportKnown()) {
+                // If one vibrator support is unknown, then the intersection is also unknown.
+                return null;
+            }
+        }
+
+        SparseBooleanArray intersection = new SparseBooleanArray();
+        SparseBooleanArray firstVibratorBraking = infos[0].getSupportedBraking();
+
+        brakingIdLoop:
+        for (int i = 0; i < firstVibratorBraking.size(); i++) {
+            int brakingId = firstVibratorBraking.keyAt(i);
+            if (!firstVibratorBraking.valueAt(i)) {
+                // The first vibrator already doesn't support this braking, so skip it.
+                continue brakingIdLoop;
+            }
+
+            for (int j = 1; j < infos.length; j++) {
+                if (!infos[j].hasBrakingSupport(brakingId)) {
+                    // One vibrator doesn't support this braking, so the intersection doesn't.
+                    continue brakingIdLoop;
+                }
+            }
+
+            intersection.put(brakingId, true);
+        }
+
+        return intersection;
+    }
+
+    @Nullable
+    private static SparseBooleanArray supportedEffectsIntersection(VibratorInfo[] infos) {
+        for (VibratorInfo info : infos) {
+            if (!info.isEffectSupportKnown()) {
+                // If one vibrator support is unknown, then the intersection is also unknown.
+                return null;
+            }
+        }
+
+        SparseBooleanArray intersection = new SparseBooleanArray();
+        SparseBooleanArray firstVibratorEffects = infos[0].getSupportedEffects();
+
+        effectIdLoop:
+        for (int i = 0; i < firstVibratorEffects.size(); i++) {
+            int effectId = firstVibratorEffects.keyAt(i);
+            if (!firstVibratorEffects.valueAt(i)) {
+                // The first vibrator already doesn't support this effect, so skip it.
+                continue effectIdLoop;
+            }
+
+            for (int j = 1; j < infos.length; j++) {
+                if (infos[j].isEffectSupported(effectId) != Vibrator.VIBRATION_EFFECT_SUPPORT_YES) {
+                    // One vibrator doesn't support this effect, so the intersection doesn't.
+                    continue effectIdLoop;
+                }
+            }
+
+            intersection.put(effectId, true);
+        }
+
+        return intersection;
+    }
+
+    @NonNull
+    private static SparseIntArray supportedPrimitivesAndDurationsIntersection(
+            VibratorInfo[] infos) {
+        SparseIntArray intersection = new SparseIntArray();
+        SparseIntArray firstVibratorPrimitives = infos[0].getSupportedPrimitives();
+
+        primitiveIdLoop:
+        for (int i = 0; i < firstVibratorPrimitives.size(); i++) {
+            int primitiveId = firstVibratorPrimitives.keyAt(i);
+            int primitiveDuration = firstVibratorPrimitives.valueAt(i);
+            if (primitiveDuration == 0) {
+                // The first vibrator already doesn't support this primitive, so skip it.
+                continue primitiveIdLoop;
+            }
+
+            for (int j = 1; j < infos.length; j++) {
+                int vibratorPrimitiveDuration = infos[j].getPrimitiveDuration(primitiveId);
+                if (vibratorPrimitiveDuration == 0) {
+                    // One vibrator doesn't support this primitive, so the intersection doesn't.
+                    continue primitiveIdLoop;
+                } else {
+                    // The primitive vibration duration is the maximum among all vibrators.
+                    primitiveDuration = Math.max(primitiveDuration, vibratorPrimitiveDuration);
+                }
+            }
+
+            intersection.put(primitiveId, primitiveDuration);
+        }
+        return intersection;
+    }
+
+    private static int integerLimitIntersection(VibratorInfo[] infos,
+            Function<VibratorInfo, Integer> propertyGetter) {
+        int limit = 0; // Limit 0 means unlimited
+        for (VibratorInfo info : infos) {
+            int vibratorLimit = propertyGetter.apply(info);
+            if ((limit == 0) || (vibratorLimit > 0 && vibratorLimit < limit)) {
+                // This vibrator is limited and intersection is unlimited or has a larger limit:
+                // use smaller limit here for the intersection.
+                limit = vibratorLimit;
+            }
+        }
+        return limit;
+    }
+
+    private static float floatPropertyIntersection(VibratorInfo[] infos,
+            Function<VibratorInfo, Float> propertyGetter) {
+        float property = propertyGetter.apply(infos[0]);
+        if (Float.isNaN(property)) {
+            // If one vibrator is undefined then the intersection is undefined.
+            return Float.NaN;
+        }
+        for (int i = 1; i < infos.length; i++) {
+            if (Float.compare(property, propertyGetter.apply(infos[i])) != 0) {
+                // If one vibrator has a different value then the intersection is undefined.
+                return Float.NaN;
+            }
+        }
+        return property;
+    }
+
+    @NonNull
+    private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) {
+        float freqResolution = floatPropertyIntersection(infos,
+                info -> info.getFrequencyProfile().getFrequencyResolutionHz());
+        float resonantFreq = floatPropertyIntersection(infos,
+                VibratorInfo::getResonantFrequencyHz);
+        Range<Float> freqRange = frequencyRangeIntersection(infos, freqResolution);
+
+        if ((freqRange == null) || Float.isNaN(freqResolution)) {
+            return new FrequencyProfile(resonantFreq, Float.NaN, freqResolution, null);
+        }
+
+        int amplitudeCount =
+                Math.round(1 + (freqRange.getUpper() - freqRange.getLower()) / freqResolution);
+        float[] maxAmplitudes = new float[amplitudeCount];
+
+        // Use MAX_VALUE here to ensure that the FrequencyProfile constructor called with this
+        // will fail if the loop below is broken and do not replace filled values with actual
+        // vibrator measurements.
+        Arrays.fill(maxAmplitudes, Float.MAX_VALUE);
+
+        for (VibratorInfo info : infos) {
+            Range<Float> vibratorFreqRange = info.getFrequencyProfile().getFrequencyRangeHz();
+            float[] vibratorMaxAmplitudes = info.getFrequencyProfile().getMaxAmplitudes();
+            int vibratorStartIdx = Math.round(
+                    (freqRange.getLower() - vibratorFreqRange.getLower()) / freqResolution);
+            int vibratorEndIdx = vibratorStartIdx + maxAmplitudes.length - 1;
+
+            if ((vibratorStartIdx < 0) || (vibratorEndIdx >= vibratorMaxAmplitudes.length)) {
+                Slog.w(TAG, "Error calculating the intersection of vibrator frequency"
+                        + " profiles: attempted to fetch from vibrator "
+                        + info.getId() + " max amplitude with bad index " + vibratorStartIdx);
+                return new FrequencyProfile(resonantFreq, Float.NaN, Float.NaN, null);
+            }
+
+            for (int i = 0; i < maxAmplitudes.length; i++) {
+                maxAmplitudes[i] = Math.min(maxAmplitudes[i],
+                        vibratorMaxAmplitudes[vibratorStartIdx + i]);
+            }
+        }
+
+        return new FrequencyProfile(resonantFreq, freqRange.getLower(),
+                freqResolution, maxAmplitudes);
+    }
+
+    @Nullable
+    private static Range<Float> frequencyRangeIntersection(VibratorInfo[] infos,
+            float frequencyResolution) {
+        Range<Float> firstRange = infos[0].getFrequencyProfile().getFrequencyRangeHz();
+        if (firstRange == null) {
+            // If one vibrator is undefined then the intersection is undefined.
+            return null;
+        }
+        float intersectionLower = firstRange.getLower();
+        float intersectionUpper = firstRange.getUpper();
+
+        // Generate the intersection of all vibrator supported ranges, making sure that both
+        // min supported frequencies are aligned w.r.t. the frequency resolution.
+
+        for (int i = 1; i < infos.length; i++) {
+            Range<Float> vibratorRange = infos[i].getFrequencyProfile().getFrequencyRangeHz();
+            if (vibratorRange == null) {
+                // If one vibrator is undefined then the intersection is undefined.
+                return null;
+            }
+
+            if ((vibratorRange.getLower() >= intersectionUpper)
+                    || (vibratorRange.getUpper() <= intersectionLower)) {
+                // If the range and intersection are disjoint then the intersection is undefined
+                return null;
+            }
+
+            float frequencyDelta = Math.abs(intersectionLower - vibratorRange.getLower());
+            if ((frequencyDelta % frequencyResolution) > EPSILON) {
+                // If the intersection is not aligned with one vibrator then it's undefined
+                return null;
+            }
+
+            intersectionLower = Math.max(intersectionLower, vibratorRange.getLower());
+            intersectionUpper = Math.min(intersectionUpper, vibratorRange.getUpper());
+        }
+
+        if ((intersectionUpper - intersectionLower) < frequencyResolution) {
+            // If the intersection is empty then it's undefined.
+            return null;
+        }
+
+        return Range.create(intersectionLower, intersectionUpper);
+    }
+}
diff --git a/core/java/android/os/vibrator/PrebakedSegment.java b/core/java/android/os/vibrator/PrebakedSegment.java
index 42b6c2da..a035092 100644
--- a/core/java/android/os/vibrator/PrebakedSegment.java
+++ b/core/java/android/os/vibrator/PrebakedSegment.java
@@ -23,6 +23,7 @@
 import android.os.Parcelable;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.VibratorInfo;
 
 import java.util.Objects;
 
@@ -77,8 +78,8 @@
 
     /** @hide */
     @Override
-    public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) {
-        if (vibrator.areAllEffectsSupported(mEffectId) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES) {
+    public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
+        if (vibratorInfo.isEffectSupported(mEffectId) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES) {
             return true;
         }
         if (!mFallback) {
diff --git a/core/java/android/os/vibrator/PrimitiveSegment.java b/core/java/android/os/vibrator/PrimitiveSegment.java
index c52a09c..95d97bf 100644
--- a/core/java/android/os/vibrator/PrimitiveSegment.java
+++ b/core/java/android/os/vibrator/PrimitiveSegment.java
@@ -22,7 +22,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
+import android.os.VibratorInfo;
 
 import com.android.internal.util.Preconditions;
 
@@ -77,8 +77,8 @@
 
     /** @hide */
     @Override
-    public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) {
-        return vibrator.areAllPrimitivesSupported(mPrimitiveId);
+    public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
+        return vibratorInfo.isPrimitiveSupported(mPrimitiveId);
     }
 
     /** @hide */
diff --git a/core/java/android/os/vibrator/RampSegment.java b/core/java/android/os/vibrator/RampSegment.java
index e997bcd..5f9d102 100644
--- a/core/java/android/os/vibrator/RampSegment.java
+++ b/core/java/android/os/vibrator/RampSegment.java
@@ -20,7 +20,7 @@
 import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
+import android.os.VibratorInfo;
 
 import com.android.internal.util.Preconditions;
 
@@ -96,7 +96,7 @@
 
     /** @hide */
     @Override
-    public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) {
+    public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
         boolean areFeaturesSupported = true;
         // If the start/end frequencies are not the same, require frequency control since we need to
         // ramp up/down the frequency.
@@ -104,7 +104,7 @@
                 // If there is no frequency ramping, make sure that the one frequency used does not
                 // require frequency control.
                 || frequencyRequiresFrequencyControl(mStartFrequencyHz)) {
-            areFeaturesSupported &= vibrator.hasFrequencyControl();
+            areFeaturesSupported &= vibratorInfo.hasFrequencyControl();
         }
         // If the start/end amplitudes are not the same, require amplitude control since we need to
         // ramp up/down the amplitude.
@@ -112,7 +112,7 @@
                 // If there is no amplitude ramping, make sure that the amplitude used does not
                 // require amplitude control.
                 || amplitudeRequiresAmplitudeControl(mStartAmplitude)) {
-            areFeaturesSupported &= vibrator.hasAmplitudeControl();
+            areFeaturesSupported &= vibratorInfo.hasAmplitudeControl();
         }
         return areFeaturesSupported;
     }
diff --git a/core/java/android/os/vibrator/StepSegment.java b/core/java/android/os/vibrator/StepSegment.java
index a585aa8..9576a5b 100644
--- a/core/java/android/os/vibrator/StepSegment.java
+++ b/core/java/android/os/vibrator/StepSegment.java
@@ -21,7 +21,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
+import android.os.VibratorInfo;
 
 import com.android.internal.util.Preconditions;
 
@@ -82,13 +82,13 @@
 
     /** @hide */
     @Override
-    public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) {
+    public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
         boolean areFeaturesSupported = true;
         if (frequencyRequiresFrequencyControl(mFrequencyHz)) {
-            areFeaturesSupported &= vibrator.hasFrequencyControl();
+            areFeaturesSupported &= vibratorInfo.hasFrequencyControl();
         }
         if (amplitudeRequiresAmplitudeControl(mAmplitude)) {
-            areFeaturesSupported &= vibrator.hasAmplitudeControl();
+            areFeaturesSupported &= vibratorInfo.hasAmplitudeControl();
         }
         return areFeaturesSupported;
     }
diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java
index 3b286a7..17ac36f 100644
--- a/core/java/android/os/vibrator/VibrationEffectSegment.java
+++ b/core/java/android/os/vibrator/VibrationEffectSegment.java
@@ -21,7 +21,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
+import android.os.VibratorInfo;
 
 /**
  * Representation of a single segment of a {@link VibrationEffect}.
@@ -65,7 +65,7 @@
      *
      * @hide
      */
-    public abstract boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator);
+    public abstract boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo);
 
     /**
      * Returns true if this segment could be a haptic feedback effect candidate.
diff --git a/core/java/android/os/vibrator/VibratorInfoFactory.java b/core/java/android/os/vibrator/VibratorInfoFactory.java
new file mode 100644
index 0000000..d10d7ec
--- /dev/null
+++ b/core/java/android/os/vibrator/VibratorInfoFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 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.annotation.NonNull;
+import android.os.VibratorInfo;
+
+/**
+ * Factory for creating {@link VibratorInfo}s.
+ *
+ * @hide
+ */
+public final class VibratorInfoFactory {
+    /**
+     * Creates a single {@link VibratorInfo} that is an intersection of a given collection of
+     * {@link VibratorInfo}s. That is, the capabilities of the returned info will be an
+     * intersection of that of the provided infos.
+     *
+     * @param id the ID for the new {@link VibratorInfo}.
+     * @param vibratorInfos the {@link VibratorInfo}s from which to create a single
+     *      {@link VibratorInfo}.
+     * @return a {@link VibratorInfo} that represents the intersection of {@code vibratorInfos}.
+     */
+    @NonNull
+    public static VibratorInfo create(int id, @NonNull VibratorInfo[] vibratorInfos) {
+        if (vibratorInfos.length == 0) {
+            return new VibratorInfo.Builder(id).build();
+        }
+        if (vibratorInfos.length == 1) {
+            // Create an equivalent info with the requested ID.
+            return new VibratorInfo(id, vibratorInfos[0]);
+        }
+        // Create a MultiVibratorInfo that intersects all the given infos and has the requested ID.
+        return new MultiVibratorInfo(id, vibratorInfos);
+    }
+
+    private VibratorInfoFactory() {}
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 50ebab7..abc8bf5 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10657,6 +10657,14 @@
                 "assist_long_press_home_enabled";
 
         /**
+         * Whether press and hold on nav handle can trigger search.
+         *
+         * @hide
+         */
+        public static final String SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED =
+                "search_press_hold_nav_handle_enabled";
+
+        /**
          * Control whether Trust Agents are in active unlock or extend unlock mode.
          * @hide
          */
diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java
index 75640bd..f3b4c6d 100644
--- a/core/java/android/service/notification/NotificationRankingUpdate.java
+++ b/core/java/android/service/notification/NotificationRankingUpdate.java
@@ -92,6 +92,7 @@
                 mapParcel.recycle();
                 if (buffer != null) {
                     mRankingMapFd.unmap(buffer);
+                    mRankingMapFd.close();
                 }
             }
         } else {
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 21f676e..94d8516 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -67,6 +67,7 @@
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
+import com.android.internal.infra.AndroidFuture;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -1710,6 +1711,11 @@
             Slog.i(TAG, "onProcessRestarted");
             mHandler.sendEmptyMessage(MSG_PROCESS_RESTARTED);
         }
+
+        @Override
+        public void onOpenFile(String filename, AndroidFuture future) throws RemoteException {
+            throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
+        }
     }
 
     void onDetectorRemoteException() {
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index d9ee859..ccf8b67 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -227,6 +227,12 @@
         public void stopDetection() {
             HotwordDetectionService.this.onStopDetection();
         }
+
+        @Override
+        public void registerRemoteStorageService(IDetectorSessionStorageService
+                detectorSessionStorageService) {
+            throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
+        }
     };
 
     @Override
diff --git a/core/java/android/service/voice/IDetectorSessionStorageService.aidl b/core/java/android/service/voice/IDetectorSessionStorageService.aidl
new file mode 100644
index 0000000..592373e
--- /dev/null
+++ b/core/java/android/service/voice/IDetectorSessionStorageService.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.service.voice;
+
+import com.android.internal.infra.AndroidFuture;
+
+/**
+ * @hide
+ */
+oneway interface IDetectorSessionStorageService {
+    /**
+     * Called when a file open request is sent. Only files with the given names under the internal
+     * app storage, i.e., {@link Context#getFilesDir()} can be opened.
+     */
+    void openFile(in String filename, in AndroidFuture future);
+}
diff --git a/core/java/android/service/voice/ISandboxedDetectionService.aidl b/core/java/android/service/voice/ISandboxedDetectionService.aidl
index 098536d..c76ac28 100644
--- a/core/java/android/service/voice/ISandboxedDetectionService.aidl
+++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl
@@ -24,6 +24,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.os.SharedMemory;
+import android.service.voice.IDetectorSessionStorageService;
 import android.service.voice.IDetectorSessionVisualQueryDetectionCallback;
 import android.service.voice.IDspHotwordDetectionCallback;
 import android.view.contentcapture.IContentCaptureManager;
@@ -71,4 +72,10 @@
     void ping(in IRemoteCallback callback);
 
     void stopDetection();
+
+    /**
+     * Registers the interface stub to talk to the voice interaction service for initialization/
+     * detection unrelated functionalities.
+     */
+    void registerRemoteStorageService(in IDetectorSessionStorageService detectorSessionStorageService);
 }
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index 128bc0d..f1bc792 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -36,6 +36,7 @@
 
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.infra.AndroidFuture;
 
 import java.io.PrintWriter;
 import java.util.concurrent.Executor;
@@ -302,6 +303,11 @@
             Binder.withCleanCallingIdentity(() -> mExecutor.execute(
                     () -> mCallback.onHotwordDetectionServiceRestarted()));
         }
+
+        @Override
+        public void onOpenFile(String filename, AndroidFuture future) throws RemoteException {
+            throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
+        }
     }
 
     /** @hide */
diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java
index cbe7666..d184b1e 100644
--- a/core/java/android/service/voice/VisualQueryDetectionService.java
+++ b/core/java/android/service/voice/VisualQueryDetectionService.java
@@ -40,7 +40,12 @@
 import android.view.contentcapture.ContentCaptureManager;
 import android.view.contentcapture.IContentCaptureManager;
 
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.util.Objects;
+import java.util.concurrent.ExecutionException;
 import java.util.function.IntConsumer;
 
 /**
@@ -86,6 +91,8 @@
     private ContentCaptureManager mContentCaptureManager;
     @Nullable
     private IRecognitionServiceManager mIRecognitionServiceManager;
+    @Nullable
+    private IDetectorSessionStorageService mDetectorSessionStorageService;
 
 
     private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() {
@@ -154,6 +161,12 @@
         public void updateRecognitionServiceManager(IRecognitionServiceManager manager) {
             mIRecognitionServiceManager = manager;
         }
+
+        @Override
+        public void registerRemoteStorageService(IDetectorSessionStorageService
+                detectorSessionStorageService) {
+            mDetectorSessionStorageService = detectorSessionStorageService;
+        }
     };
 
     @Override
@@ -323,4 +336,23 @@
         }
     }
 
+    /**
+     * Overrides {@link Context#openFileInput} to read files with the given file names under the
+     * internal app storage of the {@link VoiceInteractionService}, i.e., only files stored in
+     * {@link Context#getFilesDir()} can be opened.
+     */
+    @Override
+    public @Nullable FileInputStream openFileInput(@NonNull String filename) throws
+            FileNotFoundException {
+        try {
+            AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>();
+            mDetectorSessionStorageService.openFile(filename, future);
+            ParcelFileDescriptor pfd = future.get();
+            return new FileInputStream(pfd.getFileDescriptor());
+        } catch (RemoteException | ExecutionException | InterruptedException e) {
+            Log.w(TAG, "Cannot open file due to remote service failure");
+            throw new FileNotFoundException(e.getMessage());
+        }
+    }
+
 }
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index 9e0eb4b..b5448d4 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -25,6 +25,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.content.Context;
 import android.hardware.soundtrigger.SoundTrigger;
 import android.media.AudioFormat;
 import android.os.Binder;
@@ -37,7 +38,10 @@
 
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.infra.AndroidFuture;
 
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.PrintWriter;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -58,18 +62,20 @@
 
     private final Callback mCallback;
     private final Executor mExecutor;
+    private final Context mContext;
     private final IVoiceInteractionManagerService mManagerService;
     private final VisualQueryDetectorInitializationDelegate mInitializationDelegate;
     private final String mAttributionTag;
 
     VisualQueryDetector(
             IVoiceInteractionManagerService managerService,
-            @NonNull @CallbackExecutor Executor executor,
-            Callback callback, @Nullable String attributionTag) {
+            @NonNull @CallbackExecutor Executor executor, Callback callback, Context context,
+            @Nullable String attributionTag) {
         mManagerService = managerService;
         mCallback = callback;
         mExecutor = executor;
         mInitializationDelegate = new VisualQueryDetectorInitializationDelegate();
+        mContext = context;
         mAttributionTag = attributionTag;
     }
 
@@ -247,7 +253,7 @@
         @Override
         void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {
             initAndVerifyDetector(options, sharedMemory,
-                    new InitializationStateListener(mExecutor, mCallback),
+                    new InitializationStateListener(mExecutor, mCallback, mContext),
                     DETECTOR_TYPE_VISUAL_QUERY_DETECTOR, mAttributionTag);
         }
 
@@ -332,9 +338,12 @@
         private final Executor mExecutor;
         private final Callback mCallback;
 
-        InitializationStateListener(Executor executor, Callback callback) {
+        private final Context mContext;
+
+        InitializationStateListener(Executor executor, Callback callback, Context context) {
             this.mExecutor = executor;
             this.mCallback = callback;
+            this.mContext = context;
         }
 
         @Override
@@ -428,5 +437,22 @@
                         !TextUtils.isEmpty(errorMessage) ? errorMessage : "Error data is null");
             }));
         }
+        @Override
+        public void onOpenFile(String filename, AndroidFuture future) throws RemoteException {
+            Slog.v(TAG, "BinderCallback#onOpenFile " + filename);
+            Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> {
+                Slog.v(TAG, "onOpenFile: " + filename);
+                File f = new File(mContext.getFilesDir(), filename);
+                ParcelFileDescriptor pfd = null;
+                try {
+                    Slog.d(TAG, "opened a file with ParcelFileDescriptor.");
+                    pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
+                } catch (FileNotFoundException e) {
+                    Slog.e(TAG, "Cannot open file. No ParcelFileDescriptor returned.");
+                } finally {
+                    future.complete(pfd);
+                }
+            }));
+        }
     }
 }
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index 8cec17f..b48b7ec 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -965,7 +965,7 @@
             }
 
             VisualQueryDetector visualQueryDetector =
-                    new VisualQueryDetector(mSystemService, executor, callback,
+                    new VisualQueryDetector(mSystemService, executor, callback, this,
                             getAttributionTag());
             HotwordDetector visualQueryDetectorInitializationDelegate =
                     visualQueryDetector.getInitializationDelegate();
diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java
index 9656f36..7f313c1 100644
--- a/core/java/android/speech/RecognitionService.java
+++ b/core/java/android/speech/RecognitionService.java
@@ -38,6 +38,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.lang.ref.WeakReference;
@@ -232,39 +233,68 @@
                     intent,
                     attributionSource,
                     new ModelDownloadListener() {
+
+                        private final Object mLock = new Object();
+
+                        @GuardedBy("mLock")
+                        private boolean mIsTerminated = false;
+
                         @Override
                         public void onProgress(int completedPercent) {
-                            try {
-                                listener.onProgress(completedPercent);
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                try {
+                                    listener.onProgress(completedPercent);
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
 
                         @Override
                         public void onSuccess() {
-                            try {
-                                listener.onSuccess();
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                mIsTerminated = true;
+                                try {
+                                    listener.onSuccess();
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
 
                         @Override
                         public void onScheduled() {
-                            try {
-                                listener.onScheduled();
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                mIsTerminated = true;
+                                try {
+                                    listener.onScheduled();
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
 
                         @Override
                         public void onError(int error) {
-                            try {
-                                listener.onError(error);
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                mIsTerminated = true;
+                                try {
+                                    listener.onError(error);
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
                     });
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 751cd21..6e73a3c 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -19,7 +19,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.TextView;
 
@@ -78,11 +81,17 @@
     private int mConnectionCount = 0;
     private final InputMethodManager mImm;
 
+    private final RectF mTempRectF = new RectF();
+
+    private final Region mTempRegion = new Region();
+
+    private final Matrix mTempMatrix = new Matrix();
+
     /**
      * The handwrite-able View that is currently the target of a hovering stylus pointer. This is
      * used to help determine whether the handwriting PointerIcon should be shown in
      * {@link #onResolvePointerIcon(Context, MotionEvent)} so that we can reduce the number of calls
-     * to {@link #findBestCandidateView(float, float)}.
+     * to {@link #findBestCandidateView(float, float, boolean)}.
      */
     @Nullable
     private WeakReference<View> mCachedHoverTarget = null;
@@ -184,8 +193,8 @@
                 final float y = motionEvent.getY(pointerIndex);
                 if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) {
                     mState.mExceedHandwritingSlop = true;
-                    View candidateView =
-                            findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY);
+                    View candidateView = findBestCandidateView(mState.mStylusDownX,
+                            mState.mStylusDownY, /* isHover */ false);
                     if (candidateView != null) {
                         if (candidateView == getConnectedView()) {
                             if (!candidateView.hasFocus()) {
@@ -393,13 +402,14 @@
             final View cachedHoverTarget = getCachedHoverTarget();
             if (cachedHoverTarget != null) {
                 final Rect handwritingArea = getViewHandwritingArea(cachedHoverTarget);
-                if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget)
+                if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget,
+                        /* isHover */ true)
                         && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) {
                     return cachedHoverTarget;
                 }
             }
 
-            final View candidateView = findBestCandidateView(hoverX, hoverY);
+            final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true);
 
             if (candidateView != null) {
                 mCachedHoverTarget = new WeakReference<>(candidateView);
@@ -429,14 +439,14 @@
      * @param y the y coordinates of the stylus event, in the coordinates of the window.
      */
     @Nullable
-    private View findBestCandidateView(float x, float y) {
+    private View findBestCandidateView(float x, float y, boolean isHover) {
         // If the connectedView is not null and do not set any handwriting area, it will check
         // whether the connectedView's boundary contains the initial stylus position. If true,
         // directly return the connectedView.
         final View connectedView = getConnectedView();
         if (connectedView != null) {
             Rect handwritingArea = getViewHandwritingArea(connectedView);
-            if (isInHandwritingArea(handwritingArea, x, y, connectedView)
+            if (isInHandwritingArea(handwritingArea, x, y, connectedView, isHover)
                     && shouldTriggerStylusHandwritingForView(connectedView)) {
                 return connectedView;
             }
@@ -450,7 +460,7 @@
         for (HandwritableViewInfo viewInfo : handwritableViewInfos) {
             final View view = viewInfo.getView();
             final Rect handwritingArea = viewInfo.getHandwritingArea();
-            if (!isInHandwritingArea(handwritingArea, x, y, view)
+            if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
                     || !shouldTriggerStylusHandwritingForView(view)) {
                 continue;
             }
@@ -546,15 +556,48 @@
      * Return true if the (x, y) is inside by the given {@link Rect} with the View's
      * handwriting bounds with offsets applied.
      */
-    private static boolean isInHandwritingArea(@Nullable Rect handwritingArea,
-            float x, float y, View view) {
+    private boolean isInHandwritingArea(@Nullable Rect handwritingArea,
+            float x, float y, View view, boolean isHover) {
         if (handwritingArea == null) return false;
 
-        return contains(handwritingArea, x, y,
+        if (!contains(handwritingArea, x, y,
                 view.getHandwritingBoundsOffsetLeft(),
                 view.getHandwritingBoundsOffsetTop(),
                 view.getHandwritingBoundsOffsetRight(),
-                view.getHandwritingBoundsOffsetBottom());
+                view.getHandwritingBoundsOffsetBottom())) {
+            return false;
+        }
+
+        // The returned handwritingArea computed by ViewParent#getChildVisibleRect didn't consider
+        // the case where a view is stacking on top of the editor. (e.g. DrawerLayout, popup)
+        // We must check the hit region of the editor again, and avoid the case where another
+        // view on top of the editor is handling MotionEvents.
+        ViewParent parent = view.getParent();
+        if (parent == null) {
+            return true;
+        }
+
+        Region region = mTempRegion;
+        mTempRegion.set(0, 0, view.getWidth(), view.getHeight());
+        Matrix matrix = mTempMatrix;
+        matrix.reset();
+        if (!parent.getChildLocalHitRegion(view, region, matrix, isHover)) {
+            return false;
+        }
+
+        // It's not easy to extend the region by the given handwritingBoundsOffset. Instead, we
+        // create a rectangle surrounding the motion event location and check if this rectangle
+        // overlaps with the hit region of the editor.
+        float left = x - view.getHandwritingBoundsOffsetRight();
+        float top = y - view.getHandwritingBoundsOffsetBottom();
+        float right = Math.max(x + view.getHandwritingBoundsOffsetLeft(), left + 1);
+        float bottom =  Math.max(y + view.getHandwritingBoundsOffsetTop(), top + 1);
+        RectF rectF = mTempRectF;
+        rectF.set(left, top, right, bottom);
+        matrix.mapRect(rectF);
+
+        return region.op(Math.round(rectF.left), Math.round(rectF.top),
+                Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT);
     }
 
     /**
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 6c5f195..fabfed3 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -28,7 +28,6 @@
 import static android.view.InsetsAnimationControlImplProto.PENDING_INSETS;
 import static android.view.InsetsAnimationControlImplProto.SHOWN_ON_FINISH;
 import static android.view.InsetsAnimationControlImplProto.TMP_MATRIX;
-import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
 import static android.view.InsetsController.AnimationType;
 import static android.view.InsetsController.DEBUG;
 import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
@@ -285,15 +284,11 @@
             return false;
         }
         final Insets offset = Insets.subtract(mShownInsets, mPendingInsets);
-        ArrayList<SurfaceParams> params = new ArrayList<>();
-        updateLeashesForSide(ISIDE_LEFT, offset.left, mPendingInsets.left, params, outState,
-                mPendingAlpha);
-        updateLeashesForSide(ISIDE_TOP, offset.top, mPendingInsets.top, params, outState,
-                mPendingAlpha);
-        updateLeashesForSide(ISIDE_RIGHT, offset.right, mPendingInsets.right, params, outState,
-                mPendingAlpha);
-        updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, mPendingInsets.bottom, params, outState,
-                mPendingAlpha);
+        final ArrayList<SurfaceParams> params = new ArrayList<>();
+        updateLeashesForSide(ISIDE_LEFT, offset.left, params, outState, mPendingAlpha);
+        updateLeashesForSide(ISIDE_TOP, offset.top, params, outState, mPendingAlpha);
+        updateLeashesForSide(ISIDE_RIGHT, offset.right, params, outState, mPendingAlpha);
+        updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, params, outState, mPendingAlpha);
 
         mController.applySurfaceParams(params.toArray(new SurfaceParams[params.size()]));
         mCurrentInsets = mPendingInsets;
@@ -457,7 +452,7 @@
         return alpha >= 1 ? 1 : (alpha <= 0 ? 0 : alpha);
     }
 
-    private void updateLeashesForSide(@InternalInsetsSide int side, int offset, int inset,
+    private void updateLeashesForSide(@InternalInsetsSide int side, int offset,
             ArrayList<SurfaceParams> surfaceParams, @Nullable InsetsState outState, float alpha) {
         final ArraySet<InsetsSourceControl> controls = mSideControlsMap.get(side);
         if (controls == null) {
@@ -475,9 +470,9 @@
             }
             addTranslationToMatrix(side, offset, mTmpMatrix, mTmpFrame);
 
-            final boolean visible = mHasZeroInsetsIme && side == ISIDE_BOTTOM
-                    ? (mAnimationType == ANIMATION_TYPE_SHOW || !mFinished)
-                    : inset != 0;
+            final boolean visible = mPendingFraction == 0 && source != null
+                    ? source.isVisible()
+                    : !mFinished || mShownOnFinish;
 
             if (outState != null && source != null) {
                 outState.addSource(new InsetsSource(source)
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index c6d8bd1..8ec7d67 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -666,9 +666,6 @@
     /** Set of inset types for which an animation was started since last resetting this field */
     private @InsetsType int mLastStartedAnimTypes;
 
-    /** Set of inset types which cannot be controlled by the user animation */
-    private @InsetsType int mDisabledUserAnimationInsetsTypes;
-
     /** Set of inset types which are existing */
     private @InsetsType int mExistingTypes = 0;
 
@@ -887,21 +884,11 @@
         mState.set(newState, 0 /* types */);
         @InsetsType int existingTypes = 0;
         @InsetsType int visibleTypes = 0;
-        @InsetsType int disabledUserAnimationTypes = 0;
         @InsetsType int[] cancelledUserAnimationTypes = {0};
         for (int i = 0, size = newState.sourceSize(); i < size; i++) {
             final InsetsSource source = newState.sourceAt(i);
             @InsetsType int type = source.getType();
             @AnimationType int animationType = getAnimationType(type);
-            if (!source.isUserControllable()) {
-                // The user animation is not allowed when visible frame is empty.
-                disabledUserAnimationTypes |= type;
-                if (animationType == ANIMATION_TYPE_USER) {
-                    // Existing user animation needs to be cancelled.
-                    animationType = ANIMATION_TYPE_NONE;
-                    cancelledUserAnimationTypes[0] |= type;
-                }
-            }
             final InsetsSourceConsumer consumer = mSourceConsumers.get(source.getId());
             if (consumer != null) {
                 consumer.updateSource(source, animationType);
@@ -931,28 +918,11 @@
         }
         InsetsState.traverse(mState, newState, mRemoveGoneSources);
 
-        updateDisabledUserAnimationTypes(disabledUserAnimationTypes);
-
         if (cancelledUserAnimationTypes[0] != 0) {
             mHandler.post(() -> show(cancelledUserAnimationTypes[0]));
         }
     }
 
-    private void updateDisabledUserAnimationTypes(@InsetsType int disabledUserAnimationTypes) {
-        @InsetsType int diff = mDisabledUserAnimationInsetsTypes ^ disabledUserAnimationTypes;
-        if (diff != 0) {
-            for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
-                InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
-                if (consumer.getControl() != null && (consumer.getType() & diff) != 0) {
-                    mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
-                    mHandler.post(mInvokeControllableInsetsChangedListeners);
-                    break;
-                }
-            }
-            mDisabledUserAnimationInsetsTypes = disabledUserAnimationTypes;
-        }
-    }
-
     private boolean captionInsetsUnchanged() {
         if (CAPTION_ON_SHELL) {
             return false;
@@ -1332,26 +1302,6 @@
                     + " while an existing " + Type.toString(mTypesBeingCancelled)
                     + " is being cancelled.");
         }
-        if (animationType == ANIMATION_TYPE_USER) {
-            final @InsetsType int disabledTypes = types & mDisabledUserAnimationInsetsTypes;
-            if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes);
-            types &= ~mDisabledUserAnimationInsetsTypes;
-
-            if ((disabledTypes & ime()) != 0) {
-                ImeTracker.forLogging().onFailed(statsToken,
-                        ImeTracker.PHASE_CLIENT_DISABLED_USER_ANIMATION);
-
-                if (fromIme
-                        && !mState.isSourceOrDefaultVisible(mImeSourceConsumer.getId(), ime())) {
-                    // We've requested IMM to show IME, but the IME is not controllable. We need to
-                    // cancel the request.
-                    setRequestedVisibleTypes(0 /* visibleTypes */, ime());
-                    if (mImeSourceConsumer.onAnimationStateChanged(false /* running */)) {
-                        notifyVisibilityChanged();
-                    }
-                }
-            }
-        }
         if (types == 0) {
             // nothing to animate.
             listener.onCancelled(null);
@@ -1954,7 +1904,7 @@
         for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
             InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
             InsetsSource source = mState.peekSource(consumer.getId());
-            if (consumer.getControl() != null && source != null && source.isUserControllable()) {
+            if (consumer.getControl() != null && source != null) {
                 result |= consumer.getType();
             }
         }
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index ff009ed..0d5704e 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -187,11 +187,6 @@
         return (mFlags & flags) == flags;
     }
 
-    boolean isUserControllable() {
-        // If mVisibleFrame is null, it will be the same area as mFrame.
-        return mVisibleFrame == null || !mVisibleFrame.isEmpty();
-    }
-
     /**
      * Calculates the insets this source will cause to a client window.
      *
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 92509c9..5e19c67 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -9279,8 +9279,8 @@
             }
 
             while (parentGroup != null && !parentGroup.isImportantForAutofill()) {
-                ignoredParentLeft += parentGroup.mLeft;
-                ignoredParentTop += parentGroup.mTop;
+                ignoredParentLeft += parentGroup.mLeft - parentGroup.mScrollX;
+                ignoredParentTop += parentGroup.mTop - parentGroup.mScrollY;
 
                 viewParent = parentGroup.getParent();
                 if (viewParent instanceof View) {
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 1b1098d..7bdff8c 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -7361,6 +7361,90 @@
         }
     }
 
+    /**
+     * @hide
+     */
+    @Override
+    public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region,
+            @NonNull Matrix matrix, boolean isHover) {
+        if (!child.hasIdentityMatrix()) {
+            matrix.preConcat(child.getInverseMatrix());
+        }
+
+        final int dx = child.mLeft - mScrollX;
+        final int dy = child.mTop - mScrollY;
+        matrix.preTranslate(-dx, -dy);
+
+        final int width = mRight - mLeft;
+        final int height = mBottom - mTop;
+
+        // Map the bounds of this view into the region's coordinates and clip the region.
+        final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF();
+        rect.set(0, 0, width, height);
+        matrix.mapRect(rect);
+
+        boolean notEmpty = region.op(Math.round(rect.left), Math.round(rect.top),
+                Math.round(rect.right), Math.round(rect.bottom), Region.Op.INTERSECT);
+
+        if (isHover) {
+            HoverTarget target = mFirstHoverTarget;
+            boolean childIsHit = false;
+            while (target != null) {
+                final HoverTarget next = target.next;
+                if (target.child == child) {
+                    childIsHit = true;
+                    break;
+                }
+                target = next;
+            }
+            if (!childIsHit) {
+                target = mFirstHoverTarget;
+                while (notEmpty && target != null) {
+                    final HoverTarget next = target.next;
+                    final View hoveredView = target.child;
+
+                    rect.set(hoveredView.mLeft, hoveredView.mTop, hoveredView.mRight,
+                            hoveredView.mBottom);
+                    matrix.mapRect(rect);
+                    notEmpty = region.op(Math.round(rect.left), Math.round(rect.top),
+                            Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE);
+                    target = next;
+                }
+            }
+        } else {
+            TouchTarget target = mFirstTouchTarget;
+            boolean childIsHit = false;
+            while (target != null) {
+                final TouchTarget next = target.next;
+                if (target.child == child) {
+                    childIsHit = true;
+                    break;
+                }
+                target = next;
+            }
+            if (!childIsHit) {
+                target = mFirstTouchTarget;
+                while (notEmpty && target != null) {
+                    final TouchTarget next = target.next;
+                    final View touchedView = target.child;
+
+                    rect.set(touchedView.mLeft, touchedView.mTop, touchedView.mRight,
+                            touchedView.mBottom);
+                    matrix.mapRect(rect);
+                    notEmpty = region.op(Math.round(rect.left), Math.round(rect.top),
+                            Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE);
+                    target = next;
+                }
+            }
+        }
+
+        if (notEmpty && mParent != null) {
+            notEmpty = mParent.getChildLocalHitRegion(this, region, matrix, isHover);
+        }
+        return notEmpty;
+    }
+
+
     private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) {
         final int[] locationInWindow = new int[2];
         view.getLocationInWindow(locationInWindow);
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index 1020d2e..54bc348 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Bundle;
@@ -686,6 +687,36 @@
     }
 
     /**
+     * Compute the region where the child can receive the {@link MotionEvent}s from the root view.
+     *
+     * <p> Given region where the child will accept {@link MotionEvent}s.
+     * Modify the region to the unblocked region where the child can receive the
+     * {@link MotionEvent}s from the view root.
+     * </p>
+     *
+     * <p> The given region is always clipped by the bounds of the parent views. When there are
+     * on-going {@link MotionEvent}s, this method also makes use of the event dispatching results to
+     * determine whether a sibling view will also block the child's hit region.
+     * </p>
+     *
+     * @param child a child View, whose hit region we want to compute.
+     * @param region the initial hit region where the child view will handle {@link MotionEvent}s,
+     *              defined in the child coordinates. Will be overwritten to the result hit region.
+     * @param matrix the matrix that maps the given child view's coordinates to the region
+     *               coordinates. It will be modified to a matrix that maps window coordinates to
+     *               the result region's coordinates.
+     * @param isHover if true it will return the hover events' hit region, otherwise it will
+     *               return the touch events' hit region.
+     * @return true if the returned region is not empty.
+     * @hide
+     */
+    default boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region,
+            @NonNull Matrix matrix, boolean isHover) {
+        region.setEmpty();
+        return false;
+    }
+
+    /**
      * Unbuffered dispatch has been requested by a child of this view parent.
      * This method is called by the View hierarchy to signal ancestors that a View needs to
      * request unbuffered dispatch.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 3aa610a..ddd3269 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -128,6 +128,7 @@
 import android.graphics.PorterDuff;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.Region;
 import android.graphics.RenderNode;
 import android.graphics.drawable.Drawable;
@@ -321,13 +322,6 @@
             SystemProperties.getBoolean("persist.wm.debug.client_immersive_confirmation", false);
 
     /**
-     * Whether the client should compute the window frame on its own.
-     * @hide
-     */
-    public static final boolean LOCAL_LAYOUT =
-            SystemProperties.getBoolean("persist.debug.local_layout", true);
-
-    /**
      * Set this system property to true to force the view hierarchy to render
      * at 60 Hz. This can be used to measure the potential framerate.
      */
@@ -1911,8 +1905,8 @@
         final float compatScale = frames.compatScale;
         final boolean frameChanged = !mWinFrame.equals(frame);
         final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
-        final boolean attachedFrameChanged = LOCAL_LAYOUT
-                && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
+        final boolean attachedFrameChanged =
+                !Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
         final boolean displayChanged = mDisplay.getDisplayId() != displayId;
         final boolean compatScaleChanged = mTmpFrames.compatScale != compatScale;
         final boolean dragResizingChanged = mPendingDragResizing != dragResizing;
@@ -2397,6 +2391,22 @@
     }
 
     @Override
+    public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region,
+            @NonNull Matrix matrix, boolean isHover) {
+        if (child != mView) {
+            throw new IllegalArgumentException("child " + child + " is not the root view "
+                    + mView + " managed by this ViewRootImpl");
+        }
+
+        RectF rectF = new RectF(0, 0, mWidth, mHeight);
+        matrix.mapRect(rectF);
+        // Note: don't apply scroll offset, because we want to know its
+        // visibility in the virtual canvas being given to the view hierarchy.
+        return region.op(Math.round(rectF.left), Math.round(rectF.top),
+                Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT);
+    }
+
+    @Override
     public void bringChildToFront(View child) {
     }
 
@@ -8292,8 +8302,7 @@
         final int measuredWidth = mMeasuredWidth;
         final int measuredHeight = mMeasuredHeight;
         final boolean relayoutAsync;
-        if (LOCAL_LAYOUT
-                && (mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0
+        if ((mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0
                 && mWindowAttributes.type != TYPE_APPLICATION_STARTING
                 && mSyncSeqId <= mLastSyncSeqId
                 && winConfigFromAm.diff(winConfigFromWm, false /* compareUndefined */) == 0) {
diff --git a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
index 3801188..ba87caa 100644
--- a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
+++ b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
@@ -22,6 +22,7 @@
 import android.service.voice.HotwordRejectedResult;
 import android.service.voice.SoundTriggerFailure;
 import android.service.voice.VisualQueryDetectionServiceFailure;
+import com.android.internal.infra.AndroidFuture;
 
 /**
  * @hide
@@ -113,4 +114,9 @@
 
     /** Called when the hotword detection process is restarted */
     void onProcessRestarted();
+
+    /**
+     * Called when a file open request is sent.
+     */
+   void onOpenFile(in String filename, in AndroidFuture future);
 }
diff --git a/core/java/com/android/internal/widget/BigPictureNotificationImageView.java b/core/java/com/android/internal/widget/BigPictureNotificationImageView.java
index 3a7cf74..f95f5db 100644
--- a/core/java/com/android/internal/widget/BigPictureNotificationImageView.java
+++ b/core/java/com/android/internal/widget/BigPictureNotificationImageView.java
@@ -37,13 +37,16 @@
  * Icon.loadDrawable().
  */
 @RemoteViews.RemoteView
-public class BigPictureNotificationImageView extends ImageView {
+public class BigPictureNotificationImageView extends ImageView implements
+        NotificationDrawableConsumer {
 
     private static final String TAG = BigPictureNotificationImageView.class.getSimpleName();
 
     private final int mMaximumDrawableWidth;
     private final int mMaximumDrawableHeight;
 
+    private NotificationIconManager mIconManager;
+
     public BigPictureNotificationImageView(@NonNull Context context) {
         this(context, null, 0, 0);
     }
@@ -69,6 +72,19 @@
                         : R.dimen.notification_big_picture_max_height);
     }
 
+
+    /**
+     * Sets an {@link NotificationIconManager} on this ImageView, which handles the loading of
+     * icons, instead of using the {@link LocalImageResolver} directly.
+     * If set, it overrides the behaviour of {@link #setImageIconAsync} and {@link #setImageIcon},
+     * and it expects that the content of this imageView is only updated calling these two methods.
+     *
+     * @param iconManager to be called, when the icon is updated
+     */
+    public void setIconManager(NotificationIconManager iconManager) {
+        mIconManager = iconManager;
+    }
+
     @Override
     @android.view.RemotableViewMethod(asyncImpl = "setImageURIAsync")
     public void setImageURI(@Nullable Uri uri) {
@@ -84,11 +100,20 @@
     @Override
     @android.view.RemotableViewMethod(asyncImpl = "setImageIconAsync")
     public void setImageIcon(@Nullable Icon icon) {
+        if (mIconManager != null) {
+            mIconManager.updateIcon(this, icon).run();
+            return;
+        }
+        // old code path
         setImageDrawable(loadImage(icon));
     }
 
     /** @hide **/
     public Runnable setImageIconAsync(@Nullable Icon icon) {
+        if (mIconManager != null) {
+            return mIconManager.updateIcon(this, icon);
+        }
+        // old code path
         final Drawable drawable = loadImage(icon);
         return () -> setImageDrawable(drawable);
     }
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index 5b6b360..42be784 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -149,6 +149,7 @@
     private View mAppNameDivider;
     private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this);
     private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
+    private boolean mPrecomputedTextEnabled = false;
 
     public ConversationLayout(@NonNull Context context) {
         super(context);
@@ -389,36 +390,37 @@
      */
     @RemotableViewMethod(asyncImpl = "setDataAsync")
     public void setData(Bundle extras) {
+        bind(parseMessagingData(extras, /* usePrecomputedText= */ false));
+    }
+
+    @NonNull
+    private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) {
         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
-        List<Notification.MessagingStyle.Message> newMessages
-                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
+        List<Notification.MessagingStyle.Message> newMessages =
+                Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
-        List<Notification.MessagingStyle.Message> newHistoricMessages
-                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+        List<Notification.MessagingStyle.Message> newHistoricMessages =
+                Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
 
         // mUser now set (would be nice to avoid the side effect but WHATEVER)
         final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class);
         // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
         RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
-                extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, android.app.RemoteInputHistoryItem.class);
+                extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS,
+                        RemoteInputHistoryItem.class);
         addRemoteInputHistoryToMessages(newMessages, history);
 
         boolean showSpinner =
                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
         int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
 
-        // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding
-        // if they exist
         final List<MessagingMessage> newMessagingMessages =
-                createMessages(newMessages, /* isHistoric= */false,
-                        /* usePrecomputedText= */false);
+                createMessages(newMessages, /* isHistoric= */false, usePrecomputedText);
         final List<MessagingMessage> newHistoricMessagingMessages =
-                createMessages(newHistoricMessages, /* isHistoric= */true,
-                        /* usePrecomputedText= */false);
-        // bind it, baby
-        bindViews(user, showSpinner, unreadCount,
-                newMessagingMessages,
-                newHistoricMessagingMessages);
+                createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText);
+
+        return new MessagingData(user, showSpinner, unreadCount,
+                newHistoricMessagingMessages, newMessagingMessages);
     }
 
     /**
@@ -430,7 +432,33 @@
      */
     @NonNull
     public Runnable setDataAsync(Bundle extras) {
-        return () -> setData(extras);
+        if (!mPrecomputedTextEnabled) {
+            return () -> setData(extras);
+        }
+
+        final MessagingData messagingData =
+                parseMessagingData(extras, /* usePrecomputedText= */ true);
+
+        return () -> {
+            finalizeInflate(messagingData.getHistoricMessagingMessages());
+            finalizeInflate(messagingData.getNewMessagingMessages());
+
+            bind(messagingData);
+        };
+    }
+
+    /**
+     * enable/disable precomputed text usage
+     * @hide
+     */
+    public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
+        mPrecomputedTextEnabled = precomputedTextEnabled;
+    }
+
+    private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) {
+        for (MessagingMessage messagingMessage : historicMessagingMessages) {
+            messagingMessage.finalizeInflate();
+        }
     }
 
     @Override
@@ -460,17 +488,12 @@
         }
     }
 
+    private void bind(MessagingData messagingData) {
+        setUser(messagingData.getUser());
+        setUnreadCount(messagingData.getUnreadCount());
 
-    private void bindViews(Person user,
-            boolean showSpinner, int unreadCount, List<MessagingMessage> newMessagingMessages,
-            List<MessagingMessage> newHistoricMessagingMessages) {
-        setUser(user);
-        setUnreadCount(unreadCount);
-        bind(showSpinner, newMessagingMessages, newHistoricMessagingMessages);
-    }
-
-    private void bind(boolean showSpinner, List<MessagingMessage> messages,
-            List<MessagingMessage> historicMessages) {
+        List<MessagingMessage> messages = messagingData.getNewMessagingMessages();
+        List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages();
         // Copy our groups, before they get clobbered
         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
 
@@ -483,7 +506,7 @@
 
         // Let's now create the views and reorder them accordingly
         //   side-effect: updates mGroups, mAddedGroups
-        createGroupViews(groups, senders, showSpinner);
+        createGroupViews(groups, senders, messagingData.getShowSpinner());
 
         // Let's first check which groups were removed altogether and remove them in one animation
         removeGroups(oldGroups);
@@ -585,7 +608,7 @@
 
             // When collapsed, we're displaying the image message in a dedicated container
             // on the right of the layout instead of inline. Let's add the isolated image there
-            MessagingGroup messagingGroup = mGroups.get(mGroups.size() -1);
+            MessagingGroup messagingGroup = mGroups.get(mGroups.size() - 1);
             MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
             if (isolatedMessage != null) {
                 newMessage = isolatedMessage.getView();
@@ -1042,7 +1065,7 @@
             }
             if (visibleChildren > 0 && group.getVisibility() == GONE) {
                 group.setVisibility(VISIBLE);
-            } else if (visibleChildren == 0 && group.getVisibility() != GONE)   {
+            } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
                 group.setVisibility(GONE);
             }
         }
@@ -1259,7 +1282,7 @@
         public boolean onTouchEvent(MotionEvent event) {
             float x = event.getX();
             float y = event.getY();
-            for (TouchDelegate delegate: mDelegates) {
+            for (TouchDelegate delegate : mDelegates) {
                 event.setLocation(x, y);
                 if (delegate.onTouchEvent(event)) {
                     return true;
diff --git a/core/java/com/android/internal/widget/MessagingData.java b/core/java/com/android/internal/widget/MessagingData.java
new file mode 100644
index 0000000..85b0201
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingData.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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.internal.widget;
+
+import android.app.Person;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+final class MessagingData {
+    private final Person mUser;
+    private final boolean mShowSpinner;
+    private final List<MessagingMessage> mHistoricMessagingMessages;
+    private final List<MessagingMessage> mNewMessagingMessages;
+    private final int mUnreadCount;
+
+    MessagingData(Person user, boolean showSpinner,
+            List<MessagingMessage> historicMessagingMessages,
+            List<MessagingMessage> newMessagingMessages) {
+        this(user, showSpinner, /* unreadCount= */0,
+                historicMessagingMessages, newMessagingMessages);
+    }
+
+    MessagingData(Person user, boolean showSpinner,
+            int unreadCount,
+            List<MessagingMessage> historicMessagingMessages,
+            List<MessagingMessage> newMessagingMessages) {
+        mUser = user;
+        mShowSpinner = showSpinner;
+        mUnreadCount = unreadCount;
+        mHistoricMessagingMessages = historicMessagingMessages;
+        mNewMessagingMessages = newMessagingMessages;
+    }
+
+    public Person getUser() {
+        return mUser;
+    }
+
+    public boolean getShowSpinner() {
+        return mShowSpinner;
+    }
+
+    public List<MessagingMessage> getHistoricMessagingMessages() {
+        return mHistoricMessagingMessages;
+    }
+
+    public List<MessagingMessage> getNewMessagingMessages() {
+        return mNewMessagingMessages;
+    }
+
+    public int getUnreadCount() {
+        return mUnreadCount;
+    }
+}
diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java
index 83557cd..b6d7503 100644
--- a/core/java/com/android/internal/widget/MessagingLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLayout.java
@@ -87,7 +87,7 @@
     private ImageResolver mImageResolver;
     private CharSequence mConversationTitle;
     private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
-
+    private boolean mPrecomputedTextEnabled = false;
     public MessagingLayout(@NonNull Context context) {
         super(context);
     }
@@ -162,15 +162,23 @@
      */
     @RemotableViewMethod(asyncImpl = "setDataAsync")
     public void setData(Bundle extras) {
+        bind(parseMessagingData(extras, /* usePrecomputedText= */false));
+    }
+
+    @NonNull
+    private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) {
         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
-        List<Notification.MessagingStyle.Message> newMessages
-                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
+        List<Notification.MessagingStyle.Message> newMessages =
+                Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
-        List<Notification.MessagingStyle.Message> newHistoricMessages
-                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
-        setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, android.app.Person.class));
-        RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
-                extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, android.app.RemoteInputHistoryItem.class);
+        List<Notification.MessagingStyle.Message> newHistoricMessages =
+                Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+        setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON,
+                Person.class));
+        RemoteInputHistoryItem[] history =
+                (RemoteInputHistoryItem[]) extras.getParcelableArray(
+                        Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS,
+                        RemoteInputHistoryItem.class);
         addRemoteInputHistoryToMessages(newMessages, history);
 
         final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class);
@@ -178,10 +186,12 @@
                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
 
         final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages,
-                /* isHistoric= */true, /* usePrecomputedText= */ false);
+                /* isHistoric= */true, usePrecomputedText);
         final List<MessagingMessage> newMessagingMessages =
-                createMessages(newMessages, /* isHistoric= */false, /* usePrecomputedText= */false);
-        bindViews(user, showSpinner, historicMessagingMessages, newMessagingMessages);
+                createMessages(newMessages, /* isHistoric */false, usePrecomputedText);
+
+        return new MessagingData(user, showSpinner,
+                historicMessagingMessages, newMessagingMessages);
     }
 
     /**
@@ -193,7 +203,32 @@
      */
     @NonNull
     public Runnable setDataAsync(Bundle extras) {
-        return () -> setData(extras);
+        if (!mPrecomputedTextEnabled) {
+            return () -> setData(extras);
+        }
+
+        final MessagingData messagingData =
+                parseMessagingData(extras, /* usePrecomputedText= */true);
+
+        return () -> {
+            finalizeInflate(messagingData.getHistoricMessagingMessages());
+            finalizeInflate(messagingData.getNewMessagingMessages());
+            bind(messagingData);
+        };
+    }
+
+    /**
+     * enable/disable precomputed text usage
+     * @hide
+     */
+    public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
+        mPrecomputedTextEnabled = precomputedTextEnabled;
+    }
+
+    private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) {
+        for (MessagingMessage messagingMessage: historicMessagingMessages) {
+            messagingMessage.finalizeInflate();
+        }
     }
 
     @Override
@@ -218,17 +253,13 @@
         }
     }
 
-    private void bindViews(Person user, boolean showSpinner,
-            List<MessagingMessage> historicMessagingMessages,
-            List<MessagingMessage> newMessagingMessages) {
-        setUser(user);
-        bind(showSpinner, historicMessagingMessages, newMessagingMessages);
-    }
+    private void bind(MessagingData messagingData) {
+        setUser(messagingData.getUser());
 
-    private void bind(boolean showSpinner, List<MessagingMessage> historicMessages,
-            List<MessagingMessage> messages) {
+        List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages();
+        List<MessagingMessage> messages = messagingData.getNewMessagingMessages();
         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
-        addMessagesToGroups(historicMessages, messages, showSpinner);
+        addMessagesToGroups(historicMessages, messages, messagingData.getShowSpinner());
 
         // Let's first check which groups were removed altogether and remove them in one animation
         removeGroups(oldGroups);
diff --git a/core/java/com/android/internal/widget/NotificationDrawableConsumer.java b/core/java/com/android/internal/widget/NotificationDrawableConsumer.java
new file mode 100644
index 0000000..7c4d929
--- /dev/null
+++ b/core/java/com/android/internal/widget/NotificationDrawableConsumer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.internal.widget;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.Nullable;
+
+/**
+ * An interface for the class, who will use {@link NotificationIconManager} to load icons.
+ */
+public interface NotificationDrawableConsumer {
+
+    /**
+     * Sets a drawable as the content of this consumer.
+     *
+     * @param drawable the {@link Drawable} to set, or {@code null} to clear the content
+     */
+    void setImageDrawable(@Nullable Drawable drawable);
+}
diff --git a/core/java/com/android/internal/widget/NotificationIconManager.java b/core/java/com/android/internal/widget/NotificationIconManager.java
new file mode 100644
index 0000000..221845c
--- /dev/null
+++ b/core/java/com/android/internal/widget/NotificationIconManager.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.internal.widget;
+
+import android.graphics.drawable.Icon;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * An interface used for Notification views to delegate handling the loading of icons.
+ */
+public interface NotificationIconManager {
+
+    /**
+     * Called when a new icon is provided to display.
+     *
+     * @param drawableConsumer a consumer, which can display the loaded drawable.
+     * @param icon             the updated icon to be displayed.
+     *
+     * @return a {@link Runnable} that sets the drawable on the consumer
+     */
+    @NonNull
+    Runnable updateIcon(
+            @NonNull NotificationDrawableConsumer drawableConsumer,
+            @Nullable Icon icon
+    );
+}
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 9aa992b..b5d70d3 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -353,7 +353,7 @@
     JNIEnv* env;
     jmethodID methodId;
 
-    ALOGD("Calling main entry %s", className.string());
+    ALOGD("Calling main entry %s", className.c_str());
 
     env = getJNIEnv();
     if (clazz == NULL || env == NULL) {
@@ -362,7 +362,7 @@
 
     methodId = env->GetStaticMethodID(clazz, "main", "([Ljava/lang/String;)V");
     if (methodId == NULL) {
-        ALOGE("ERROR: could not find method %s.main(String[])\n", className.string());
+        ALOGE("ERROR: could not find method %s.main(String[])\n", className.c_str());
         return UNKNOWN_ERROR;
     }
 
@@ -378,7 +378,7 @@
     strArray = env->NewObjectArray(numArgs, stringClass, NULL);
 
     for (size_t i = 0; i < numArgs; i++) {
-        jstring argStr = env->NewStringUTF(args[i].string());
+        jstring argStr = env->NewStringUTF(args[i].c_str());
         env->SetObjectArrayElement(strArray, i, argStr);
     }
 
@@ -1269,7 +1269,7 @@
     env->SetObjectArrayElement(strArray, 0, classNameStr);
 
     for (size_t i = 0; i < options.size(); ++i) {
-        jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
+        jstring optionsStr = env->NewStringUTF(options.itemAt(i).c_str());
         assert(optionsStr != NULL);
         env->SetObjectArrayElement(strArray, i + 1, optionsStr);
     }
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index 624bd5f..69fc515 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -292,7 +292,7 @@
     if (status) {
         String8 message;
         message.appendFormat("Failed to initialize display event receiver.  status=%d", status);
-        jniThrowRuntimeException(env, message.string());
+        jniThrowRuntimeException(env, message.c_str());
         return 0;
     }
 
@@ -316,7 +316,7 @@
     if (status) {
         String8 message;
         message.appendFormat("Failed to schedule next vertical sync pulse.  status=%d", status);
-        jniThrowRuntimeException(env, message.string());
+        jniThrowRuntimeException(env, message.c_str());
     }
 }
 
diff --git a/core/jni/android_view_InputEventSender.cpp b/core/jni/android_view_InputEventSender.cpp
index 061f669..833952d 100644
--- a/core/jni/android_view_InputEventSender.cpp
+++ b/core/jni/android_view_InputEventSender.cpp
@@ -340,7 +340,7 @@
     if (status) {
         String8 message;
         message.appendFormat("Failed to initialize input event sender.  status=%d", status);
-        jniThrowRuntimeException(env, message.string());
+        jniThrowRuntimeException(env, message.c_str());
         return 0;
     }
 
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index d7969cf..ed0081c 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -137,6 +137,7 @@
         optional SettingProto gesture_setup_complete = 9 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto touch_gesture_enabled = 10 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
     optional Assist assist = 7;
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index e129f7d..f55501a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7790,8 +7790,9 @@
                 android:process=":ui">
         </activity>
         <activity android:name="com.android.internal.app.PlatLogoActivity"
-                android:theme="@style/Theme.Wallpaper.NoTitleBar.Fullscreen"
+                android:theme="@style/Theme.NoTitleBar.Fullscreen"
                 android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+                android:enableOnBackInvokedCallback="true"
                 android:icon="@drawable/platlogo"
                 android:process=":ui">
         </activity>
diff --git a/core/res/res/drawable/focus_event_rotary_input_background.xml b/core/res/res/drawable/focus_event_rotary_input_background.xml
new file mode 100644
index 0000000..512cd68
--- /dev/null
+++ b/core/res/res/drawable/focus_event_rotary_input_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:name="focus_event_rotary_input_background"
+       android:shape="rectangle">
+
+  <!-- View background color -->
+  <solid android:color="#80741b47" />
+
+  <!-- View border color and width -->
+  <stroke android:width="1dp" android:color="#ffff00ff" />
+
+  <!-- The radius makes the corners rounded -->
+  <corners android:radius="4dp" />
+
+</shape>
\ No newline at end of file
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index a78b040..e0b6565 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -542,6 +542,9 @@
     <bool name="config_goToSleepOnButtonPressTheaterMode">true</bool>
     <!-- If this is true, long press on power button will be available from the non-interactive state -->
     <bool name="config_supportLongPressPowerWhenNonInteractive">false</bool>
+    <!-- If this is true, short press on power button will be available whenever the default display
+         is on even if the device is non-interactive (dreaming). -->
+    <bool name="config_supportShortPressPowerWhenDefaultDisplayOn">false</bool>
 
     <!-- If this is true, then keep dreaming when unplugging.
          This config was formerly known as config_keepDreamingWhenUndocking.
@@ -6172,6 +6175,9 @@
     <!-- Default value for Settings.ASSIST_TOUCH_GESTURE_ENABLED -->
     <bool name="config_assistTouchGestureEnabledDefault">true</bool>
 
+    <!-- Default value for Settings.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED -->
+    <bool name="config_searchPressHoldNavHandleEnabledDefault">true</bool>
+
     <!-- The maximum byte size of the information contained in the bundle of
     HotwordDetectedResult. -->
     <integer translatable="false" name="config_hotwordDetectedResultMaxBundleSize">0</integer>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b8f93ca..4918bbe 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1968,6 +1968,7 @@
   <java-symbol type="integer" name="config_keyguardDrawnTimeout" />
   <java-symbol type="bool" name="config_goToSleepOnButtonPressTheaterMode" />
   <java-symbol type="bool" name="config_supportLongPressPowerWhenNonInteractive" />
+  <java-symbol type="bool" name="config_supportShortPressPowerWhenDefaultDisplayOn" />
   <java-symbol type="bool" name="config_wimaxEnabled" />
   <java-symbol type="bool" name="show_ongoing_ime_switcher" />
   <java-symbol type="color" name="config_defaultNotificationColor" />
@@ -4907,6 +4908,8 @@
   <java-symbol type="bool" name="config_assistLongPressHomeEnabledDefault" />
   <java-symbol type="bool" name="config_assistTouchGestureEnabledDefault" />
 
+  <java-symbol type="bool" name="config_searchPressHoldNavHandleEnabledDefault" />
+
   <java-symbol type="integer" name="config_hotwordDetectedResultMaxBundleSize" />
 
   <java-symbol type="dimen" name="config_wallpaperDimAmount" />
@@ -5197,6 +5200,7 @@
   <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" />
 
   <java-symbol type="drawable" name="focus_event_pressed_key_background" />
+  <java-symbol type="drawable" name="focus_event_rotary_input_background" />
   <java-symbol type="string" name="config_defaultShutdownVibrationFile" />
   <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" />
 
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index c14da29..7f56eb7 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -88,7 +88,7 @@
 
     resource_dirs: ["res"],
     resource_zips: [":FrameworksCoreTests_apks_as_resources"],
-    java_resources: [":ApkVerityTestCertDer"],
+    java_resources: [":FrameworksCoreTests_unit_test_cert_der"],
 
     data: [
         ":BinderDeathRecipientHelperApp1",
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 31755ef..a358c4f 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1749,6 +1749,15 @@
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
             </intent-filter>
         </activity>
+
+        <activity android:name="android.view.ViewGroupTestActivity"
+                  android:label="ViewGroup Test"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/core/tests/coretests/certs/Android.bp b/core/tests/coretests/certs/Android.bp
index 8d4ecf4..cefdc4d 100644
--- a/core/tests/coretests/certs/Android.bp
+++ b/core/tests/coretests/certs/Android.bp
@@ -13,3 +13,8 @@
     name: "FrameworksCoreTests_unit_test_cert",
     certificate: "unit_test",
 }
+
+filegroup {
+    name: "FrameworksCoreTests_unit_test_cert_der",
+    srcs: ["unit_test.der"],
+}
diff --git a/core/tests/coretests/certs/README b/core/tests/coretests/certs/README
index 00917a1..b5c096e 100644
--- a/core/tests/coretests/certs/README
+++ b/core/tests/coretests/certs/README
@@ -2,3 +2,5 @@
 
 development/tools/make_key unit_test         '/CN=unit_test'
 development/tools/make_key unit_test_diff    '/CN=unit_test_diff'
+
+openssl x509 -in unit_test.x509.pem -out unit_test.der -outform der
diff --git a/core/tests/coretests/certs/unit_test.der b/core/tests/coretests/certs/unit_test.der
new file mode 100644
index 0000000..4dbbc49
--- /dev/null
+++ b/core/tests/coretests/certs/unit_test.der
Binary files differ
diff --git a/core/tests/coretests/res/layout/viewgroup_test.xml b/core/tests/coretests/res/layout/viewgroup_test.xml
new file mode 100644
index 0000000..04f4f52
--- /dev/null
+++ b/core/tests/coretests/res/layout/viewgroup_test.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<!-- Demonstrates adding/removing views from ViewGroup. See corresponding Java code. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:id="@+id/linear_layout"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+    <EditText
+        android:id="@+id/view"
+        android:layout_width="20dp"
+        android:layout_height="10dp"
+        android:text="Hello World!"
+        android:background="#2F00FF00" />
+    <EditText
+        android:id="@+id/view_scale"
+        android:layout_width="20dp"
+        android:layout_height="10dp"
+        android:scaleX="0.5"
+        android:scaleY="2"
+        android:transformPivotX="0dp"
+        android:transformPivotY="0dp"
+        android:text="Hello World!"
+        android:background="#2F00FF00" />
+    <EditText
+        android:id="@+id/view_translate"
+        android:layout_width="20dp"
+        android:layout_height="10dp"
+        android:translationX="10dp"
+        android:translationY="20dp"
+        android:text="Hello World!"
+        android:background="#2F00FF00" />
+    <FrameLayout
+        android:layout_width="20dp"
+        android:layout_height="10dp">
+        <EditText
+            android:id="@+id/view_overlap_bottom"
+            android:layout_width="20dp"
+            android:layout_height="10dp"
+            android:text="Hello World!"/>
+        <Button
+            android:id="@+id/view_overlap_top"
+            android:layout_width="10dp"
+            android:layout_height="10dp"/>
+    </FrameLayout>
+
+    <FrameLayout
+        android:layout_width="20dp"
+        android:layout_height="10dp">
+        <EditText
+            android:id="@+id/view_cover_bottom"
+            android:layout_width="10dp"
+            android:layout_height="10dp"
+            android:text="Hello World!"/>
+        <Button
+            android:id="@+id/view_cover_top"
+            android:layout_width="10dp"
+            android:layout_height="10dp"/>
+    </FrameLayout>
+
+</LinearLayout>
diff --git a/core/tests/coretests/res/raw/fsverity_sig b/core/tests/coretests/res/raw/fsverity_sig
index b2f335d..2c28f0b 100644
--- a/core/tests/coretests/res/raw/fsverity_sig
+++ b/core/tests/coretests/res/raw/fsverity_sig
Binary files differ
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index a936cea..f9377fc 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -19,6 +19,8 @@
 import static android.app.Notification.CarExtender.UnreadConversation.KEY_ON_READ;
 import static android.app.Notification.CarExtender.UnreadConversation.KEY_ON_REPLY;
 import static android.app.Notification.CarExtender.UnreadConversation.KEY_REMOTE_INPUT;
+import static android.app.Notification.DEFAULT_SOUND;
+import static android.app.Notification.DEFAULT_VIBRATE;
 import static android.app.Notification.EXTRA_ANSWER_INTENT;
 import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO;
 import static android.app.Notification.EXTRA_CALL_PERSON;
@@ -35,6 +37,9 @@
 import static android.app.Notification.EXTRA_PICTURE_ICON;
 import static android.app.Notification.EXTRA_SUMMARY_TEXT;
 import static android.app.Notification.EXTRA_TITLE;
+import static android.app.Notification.GROUP_ALERT_CHILDREN;
+import static android.app.Notification.GROUP_ALERT_SUMMARY;
+import static android.app.Notification.GROUP_KEY_SILENT;
 import static android.app.Notification.MessagingStyle.Message.KEY_DATA_URI;
 import static android.app.Notification.MessagingStyle.Message.KEY_SENDER_PERSON;
 import static android.app.Notification.MessagingStyle.Message.KEY_TEXT;
@@ -511,6 +516,75 @@
     }
 
     @Test
+    public void testBuilder_setSilent_summaryBehavior_groupAlertChildren() {
+        Notification summaryNotif = new Notification.Builder(mContext, "channelId")
+                .setGroupSummary(true)
+                .setGroup("groupKey")
+                .setSilent(true)
+                .build();
+        assertEquals(GROUP_ALERT_CHILDREN, summaryNotif.getGroupAlertBehavior());
+    }
+
+    @Test
+    public void testBuilder_setSilent_childBehavior_groupAlertSummary() {
+        Notification childNotif = new Notification.Builder(mContext, "channelId")
+                .setGroupSummary(false)
+                .setGroup("groupKey")
+                .setSilent(true)
+                .build();
+        assertEquals(GROUP_ALERT_SUMMARY, childNotif.getGroupAlertBehavior());
+    }
+
+    @Test
+    public void testBuilder_setSilent_emptyGroupKey_groupKeySilent() {
+        Notification emptyGroupKeyNotif = new Notification.Builder(mContext, "channelId")
+                .setGroup("")
+                .setSilent(true)
+                .build();
+        assertEquals(GROUP_KEY_SILENT, emptyGroupKeyNotif.getGroup());
+    }
+
+    @Test
+    public void testBuilder_setSilent_vibrateNull() {
+        Notification silentNotif = new Notification.Builder(mContext, "channelId")
+                .setGroup("")
+                .setSilent(true)
+                .build();
+
+        assertNull(silentNotif.vibrate);
+    }
+
+    @Test
+    public void testBuilder_setSilent_soundNull() {
+        Notification silentNotif = new Notification.Builder(mContext, "channelId")
+                .setGroup("")
+                .setSilent(true)
+                .build();
+
+        assertNull(silentNotif.sound);
+    }
+
+    @Test
+    public void testBuilder_setSilent_noDefaultSound() {
+        Notification silentNotif = new Notification.Builder(mContext, "channelId")
+                .setGroup("")
+                .setSilent(true)
+                .build();
+
+        assertEquals(0, (silentNotif.defaults & DEFAULT_SOUND));
+    }
+
+    @Test
+    public void testBuilder_setSilent_noDefaultVibrate() {
+        Notification silentNotif = new Notification.Builder(mContext, "channelId")
+                .setGroup("")
+                .setSilent(true)
+                .build();
+
+        assertEquals(0, (silentNotif.defaults & DEFAULT_VIBRATE));
+    }
+
+    @Test
     public void testCallStyle_getSystemActions_forIncomingCall() {
         PendingIntent answerIntent = createPendingIntent("answer");
         PendingIntent declineIntent = createPendingIntent("decline");
diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
index a84ac55..55ded9c 100644
--- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
+++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
@@ -136,7 +136,11 @@
         NotificationListenerService.RankingMap retrievedRankings =
                 retrievedRankingUpdate.getRankingMap();
         assertNotNull(retrievedRankings);
-        assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed());
+        // The rankingUpdate file descriptor is only non-null in the new path.
+        if (SystemUiSystemPropertiesFlags.getResolver().isEnabled(
+                SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) {
+            assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed());
+        }
         NotificationListenerService.Ranking retrievedRanking =
                 new NotificationListenerService.Ranking();
         assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking));
diff --git a/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java
new file mode 100644
index 0000000..60a0a2a
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2023 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.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+
+import android.graphics.Matrix;
+import android.graphics.Region;
+import android.platform.test.annotations.Presubmit;
+import android.widget.LinearLayout;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.filters.SmallTest;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Test basic functions of ViewGroup.
+ *
+ * Build/Install/Run:
+ *     atest FrameworksCoreTests:ViewGroupTest
+ */
+@Presubmit
+@SmallTest
+public class ViewGroupGetChildLocalHitRegionTest {
+    @Rule
+    public ActivityScenarioRule<ViewGroupTestActivity> mScenarioRule =
+            new ActivityScenarioRule<>(ViewGroupTestActivity.class);
+
+    private LinearLayout mRoot;
+    private final int[] mRootLocation = new int[2];
+
+    @Before
+    public void setup() {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            mRoot = activity.findViewById(R.id.linear_layout);
+            mRoot.getLocationInWindow(mRootLocation);
+        });
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion() {
+        assertGetChildLocalHitRegion(R.id.view);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_withScale() {
+        assertGetChildLocalHitRegion(R.id.view_scale);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_withTranslate() {
+        assertGetChildLocalHitRegion(R.id.view_translate);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_overlap_noMotionEvent() {
+        assertGetChildLocalHitRegion(R.id.view_overlap_bottom);
+    }
+    @Test
+    public void testGetChildLocalHitRegion_overlap_withMotionEvent() {
+        // In this case, view_cover_bottom is partially covered by the view_cover_top.
+        // The returned region is the bounds of the bottom view subtract the bounds of the top view.
+        assertGetChildLocalHitRegion(R.id.view_overlap_top, R.id.view_overlap_bottom);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_cover_withMotionEvent() {
+        // In this case, view_cover_bottom is completely covered by the view_cover_top.
+        // The returned region is expected to be empty.
+        assertGetChildLocalHitRegionEmpty(R.id.view_cover_top, R.id.view_cover_bottom);
+    }
+
+    private void injectMotionEvent(View view, boolean isHover) {
+        int[] location = new int[2];
+        view.getLocationInWindow(location);
+
+        float x = location[0] + view.getWidth() / 2f;
+        float y = location[1] + view.getHeight() / 2f;
+
+        int action = isHover ? MotionEvent.ACTION_HOVER_ENTER : MotionEvent.ACTION_DOWN;
+        MotionEvent motionEvent = MotionEvent.obtain(/* downtime= */ 0, /* eventTime= */ 0, action,
+                x, y, /* pressure= */ 0, /* size= */ 0, /* metaState= */ 0,
+                /* xPrecision= */ 1, /* yPrecision= */ 1, /* deviceId= */0, /* edgeFlags= */0);
+
+        View rootView = view.getRootView();
+        rootView.dispatchPointerEvent(motionEvent);
+    }
+
+    private void assertGetChildLocalHitRegion(int viewId) {
+        assertGetChildLocalHitRegion(viewId, /* isHover= */ true);
+        assertGetChildLocalHitRegion(viewId, /* isHover= */ false);
+    }
+
+    /**
+     * Assert ViewParent#getChildLocalHitRegion for a single view.
+     * @param viewId the viewId of the tested view.
+     * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit
+     *                region of the touch events.
+     */
+    private void assertGetChildLocalHitRegion(int viewId, boolean isHover) {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            View view = activity.findViewById(viewId);
+
+            Matrix actualMatrix = new Matrix();
+            Region actualRegion = new Region(0, 0, view.getWidth(), view.getHeight());
+            boolean actualNotEmpty = view.getParent()
+                    .getChildLocalHitRegion(view, actualRegion, actualMatrix, isHover);
+
+            int[] windowLocation = new int[2];
+            view.getLocationInWindow(windowLocation);
+            Matrix expectMatrix = new Matrix();
+            expectMatrix.preScale(1 / view.getScaleX(), 1 / view.getScaleY());
+            expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]);
+
+            Region expectRegion = new Region(0, 0, view.getWidth(), view.getHeight());
+
+            assertThat(actualNotEmpty).isTrue();
+            assertThat(actualMatrix).isEqualTo(expectMatrix);
+            assertThat(actualRegion).isEqualTo(expectRegion);
+        });
+    }
+
+    private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom) {
+        assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ true);
+        assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ false);
+    }
+
+    /**
+     * Assert ViewParent#getChildLocalHitRegion of a view that is covered by another view. It will
+     * inject {@link MotionEvent}s to the view on top first and then get the hit region of the
+     * bottom view.
+     *
+     * @param viewIdTop the view id of the test view on top.
+     * @param viewIdBottom the view id of the test view at the bottom.
+     * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit
+     *                region of the touch events.
+     */
+    private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom, boolean isHover) {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            View viewTop = activity.findViewById(viewIdTop);
+            View viewBottom = activity.findViewById(viewIdBottom);
+
+            injectMotionEvent(viewTop, isHover);
+
+            Matrix actualMatrix = new Matrix();
+            Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight());
+            boolean actualNotEmpty = viewBottom.getParent()
+                    .getChildLocalHitRegion(viewBottom, actualRegion, actualMatrix, isHover);
+
+            int[] windowLocation = new int[2];
+            viewBottom.getLocationInWindow(windowLocation);
+            Matrix expectMatrix = new Matrix();
+            expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]);
+
+            Region expectRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight());
+            expectRegion.op(0, 0, viewTop.getWidth(), viewTop.getHeight(), Region.Op.DIFFERENCE);
+
+            assertThat(actualNotEmpty).isTrue();
+            assertThat(actualMatrix).isEqualTo(expectMatrix);
+            assertThat(actualRegion).isEqualTo(expectRegion);
+        });
+    }
+
+    private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom) {
+        assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ true);
+        assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ false);
+    }
+
+    /**
+     * Assert ViewParent#getChildLocalHitRegion returns an empty region for a view that is
+     * completely covered by another view. It will inject {@link MotionEvent}s to the view on top
+     * first and then get the hit region of the
+     * bottom view.
+     *
+     * @param viewIdTop the view id of the test view on top.
+     * @param viewIdBottom the view id of the test view at the bottom.
+     * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit
+     *                region of the touch events.
+     */
+    private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom,
+            boolean isHover) {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            View viewTop = activity.findViewById(viewIdTop);
+            View viewBottom = activity.findViewById(viewIdBottom);
+
+            injectMotionEvent(viewTop, isHover);
+
+            Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight());
+            boolean actualNotEmpty = viewBottom.getParent()
+                    .getChildLocalHitRegion(viewBottom, actualRegion, new Matrix(), isHover);
+
+            assertThat(actualNotEmpty).isFalse();
+            assertThat(actualRegion.isEmpty()).isTrue();
+        });
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ViewGroupTestActivity.java b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java
new file mode 100644
index 0000000..b94bda5
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.view;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+public class ViewGroupTestActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.viewgroup_test);
+    }
+}
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
index 388a996..b4c72ca 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
@@ -21,7 +21,9 @@
 
 import android.app.Instrumentation;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -45,7 +47,7 @@
             float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) {
         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         final Context context = instrumentation.getTargetContext();
-        // mock a parent so that HandwritingInitiator can get
+        // mock a parent so that HandwritingInitiator can get visible rect and hit region.
         final ViewGroup parent = new ViewGroup(context) {
             @Override
             protected void onLayout(boolean changed, int l, int t, int r, int b) {
@@ -56,6 +58,14 @@
                 r.set(handwritingArea);
                 return true;
             }
+
+            @Override
+            public boolean getChildLocalHitRegion(View child, Region region, Matrix matrix,
+                    boolean isHover) {
+                matrix.reset();
+                region.set(handwritingArea);
+                return true;
+            }
         };
 
         View view = spy(new View(context));
diff --git a/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java
index 1513654..a978e3b 100644
--- a/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java
+++ b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java
@@ -229,11 +229,12 @@
     @Test
     public void testSignatureGeneratedExternally() throws Exception {
         var context = InstrumentationRegistry.getInstrumentation().getContext();
-        byte[] cert = getClass().getClassLoader().getResourceAsStream("ApkVerityTestCert.der")
+        byte[] cert = getClass().getClassLoader().getResourceAsStream("unit_test.der")
                 .readAllBytes();
         // The signature is generated by:
-        //   fsverity sign <(echo -n fs-verity) fsverity_sig --key=ApkVerityTestKey.pem \
-        //   --cert=ApkVerityTestCert.pem
+        //   openssl pkcs8 -topk8 -nocrypt -in certs/unit_test.pk8 -out certs/unit_test.key.pem
+        //   fsverity sign <(echo -n fs-verity) fsverity_sig --key=certs/unit_test.key.pem \
+        //   --cert=certs/unit_test.x509.pem
         byte[] sig = context.getResources().openRawResource(R.raw.fsverity_sig).readAllBytes();
         // The fs-verity digest is generated by:
         //   fsverity digest --compact <(echo -n fs-verity)
diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
index 8be489e..e875875 100644
--- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java
+++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
@@ -41,8 +41,6 @@
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.StepSegment;
 
-import androidx.test.InstrumentationRegistry;
-
 import com.android.internal.R;
 
 import org.jetbrains.annotations.NotNull;
@@ -838,31 +836,29 @@
 
     @Test
     public void testAreVibrationFeaturesSupported_allSegmentsSupported() {
-        Vibrator vibrator =
-                createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                        .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
-                        .build());
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build();
 
         assertTrue(VibrationEffect.createWaveform(
                         /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(VibrationEffect.createWaveform(
                         /* timings= */ new long[] {1, 2, 3},
                         /* amplitudes= */ new int[] {10, 20, 40},
                         /* repeatIndex= */ 2)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(
                 VibrationEffect.startComposition()
                         .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
                         .repeatEffectIndefinitely(TEST_ONE_SHOT)
                         .compose()
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testAreVibrationFeaturesSupported_withUnsupportedSegments() {
-        Vibrator vibrator =
-                createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1).build());
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1).build();
 
         assertFalse(
                 VibrationEffect.startComposition()
@@ -872,7 +868,7 @@
                                 /* amplitudes= */ new int[] {10, 20, 40},
                                 /* repeatIndex= */ -1))
                         .compose()
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
@@ -996,13 +992,4 @@
 
         return context;
     }
-
-    private Vibrator createVibratorWithCustomInfo(VibratorInfo info) {
-        return new SystemVibrator(InstrumentationRegistry.getContext()) {
-            @Override
-            public VibratorInfo getInfo() {
-                return info;
-            }
-        };
-    }
 }
diff --git a/core/tests/vibrator/src/android/os/VibratorInfoTest.java b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
index ff917aa..73cd464 100644
--- a/core/tests/vibrator/src/android/os/VibratorInfoTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
@@ -57,6 +57,17 @@
     }
 
     @Test
+    public void testHasFrequencyControl() {
+        VibratorInfo noCapabilities = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
+        assertFalse(noCapabilities.hasFrequencyControl());
+        VibratorInfo composeAndFrequencyControl = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
+                .setCapabilities(
+                        IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS)
+                .build();
+        assertTrue(composeAndFrequencyControl.hasFrequencyControl());
+    }
+
+    @Test
     public void testHasCapabilities() {
         VibratorInfo info = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                 .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
@@ -246,8 +257,13 @@
 
     @Test
     public void testEquals() {
-        VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
-                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+        VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID);
+        // Create a builder with a different ID, but same properties the same as the first one.
+        VibratorInfo.Builder completeBuilder2 = new VibratorInfo.Builder(TEST_VIBRATOR_ID + 2);
+
+        for (VibratorInfo.Builder builder :
+                new VibratorInfo.Builder[] {completeBuilder, completeBuilder2}) {
+            builder.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
                 .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
                 .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
                 .setPrimitiveDelayMax(100)
@@ -257,31 +273,43 @@
                 .setPwleSizeMax(20)
                 .setQFactor(2f)
                 .setFrequencyProfile(TEST_FREQUENCY_PROFILE);
+        }
         VibratorInfo complete = completeBuilder.build();
 
         assertEquals(complete, complete);
+        assertTrue(complete.equalContent(complete));
         assertEquals(complete, completeBuilder.build());
+        assertTrue(complete.equalContent(completeBuilder.build()));
         assertEquals(complete.hashCode(), completeBuilder.build().hashCode());
 
+        // The infos from the two builders should have equal content, but should not be equal due to
+        // their different IDs.
+        assertNotEquals(complete, completeBuilder2.build());
+        assertTrue(complete.equalContent(completeBuilder2.build()));
+
         VibratorInfo completeWithComposeControl = completeBuilder
                 .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
                 .build();
         assertNotEquals(complete, completeWithComposeControl);
+        assertFalse(complete.equalContent(completeWithComposeControl));
 
         VibratorInfo completeWithNoEffects = completeBuilder
                 .setSupportedEffects(new int[0])
                 .build();
         assertNotEquals(complete, completeWithNoEffects);
+        assertFalse(complete.equalContent(completeWithNoEffects));
 
         VibratorInfo completeWithUnknownEffects = completeBuilder
                 .setSupportedEffects(null)
                 .build();
         assertNotEquals(complete, completeWithUnknownEffects);
+        assertFalse(complete.equalContent(completeWithUnknownEffects));
 
         VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder
                 .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
                 .build();
         assertNotEquals(complete, completeWithDifferentPrimitiveDuration);
+        assertFalse(complete.equalContent(completeWithDifferentPrimitiveDuration));
 
         VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
@@ -291,31 +319,37 @@
                         TEST_AMPLITUDE_MAP))
                 .build();
         assertNotEquals(complete, completeWithDifferentFrequencyProfile);
+        assertFalse(complete.equalContent(completeWithDifferentFrequencyProfile));
 
         VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder
                 .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE)
                 .build();
         assertNotEquals(complete, completeWithEmptyFrequencyProfile);
+        assertFalse(complete.equalContent(completeWithEmptyFrequencyProfile));
 
         VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build();
         assertNotEquals(complete, completeWithUnknownQFactor);
+        assertFalse(complete.equalContent(completeWithUnknownQFactor));
 
         VibratorInfo completeWithDifferentQFactor = completeBuilder
                 .setQFactor(complete.getQFactor() + 3f)
                 .build();
         assertNotEquals(complete, completeWithDifferentQFactor);
+        assertFalse(complete.equalContent(completeWithDifferentQFactor));
 
         VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
         VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                 .setSupportedEffects(new int[0])
                 .build();
         assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport);
+        assertFalse(unknownEffectSupport.equalContent(knownEmptyEffectSupport));
 
         VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
         VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                 .setSupportedBraking(new int[0])
                 .build();
         assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport);
+        assertFalse(unknownBrakingSupport.equalContent(knownEmptyBrakingSupport));
     }
 
     @Test
@@ -335,4 +369,186 @@
         assertEquals(original, restored);
         assertEquals(original.hashCode(), restored.hashCode());
     }
+
+    @Test
+    public void areVibrationFeaturesSupported_noAmplitudeControl_fractionalAmplitudeUnsupported() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1).build();
+
+        // Have at least one fractional amplitude (amplitude not min (0) or max (255) or DEFAULT).
+        assertFalse(info.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30)));
+        assertFalse(info.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 255)));
+        assertFalse(info.areVibrationFeaturesSupported(
+                VibrationEffect.createOneShot(20, /* amplitude= */ 40)));
+    }
+
+    @Test
+    public void areVibrationFeaturesSupported_noAmplitudeControl_nonFractionalAmplitudeSupported() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1).build();
+
+        // All amplitudes are min, max, or default. Requires no amplitude control.
+        assertTrue(info.areVibrationFeaturesSupported(
+                waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.createWaveform(
+                        /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.createOneShot(20, /* amplitude= */ 255)));
+    }
+
+    @Test
+    public void areVibrationFeaturesSupported_withAmplitudeControl_allWaveformsSupported() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build();
+
+        // All forms of amplitudes are valid when amplitude control is available.
+        assertTrue(info.areVibrationFeaturesSupported(
+                waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.createWaveform(
+                        /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1)));
+        assertTrue(info.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30, 50)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                waveformWithAmplitudes(7, 255, 0, 0, 60)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.createOneShot(20, /* amplitude= */ 255)));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.createOneShot(20, /* amplitude= */ 40)));
+    }
+
+    @Test
+    public void areVibrationFeaturesSupported_compositionsWithSupportedPrimitivesSupported() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .compose()));
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(
+                                VibrationEffect.Composition.PRIMITIVE_CLICK,
+                                /* scale= */ 0.2f,
+                                /* delay= */ 200)
+                        .compose()));
+    }
+
+    @Test
+    public void areVibrationFeaturesSupported_compositionsWithUnupportedPrimitivesUnsupported() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+
+        assertFalse(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD)
+                        .compose()));
+        assertFalse(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)
+                        .compose()));
+    }
+
+    @Test
+    public void areVibrationFeaturesSupported_composedEffects_allComponentsSupported() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP)
+                .build();
+
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addEffect(VibrationEffect.createWaveform(
+                                /* timings= */ new long[] {1, 2, 3},
+                                /* amplitudes= */ new int[] {10, 20, 255},
+                                /* repeatIndex= */ -1))
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP))
+                        .compose()));
+
+        info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10)
+                .setSupportedEffects(VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_CLICK)
+                .build();
+
+        assertTrue(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD)
+                        .addEffect(VibrationEffect.createWaveform(
+                                // These timings are given either 0 or default amplitudes, which
+                                // do not require vibrator's amplitude control.
+                                /* timings= */ new long[] {1, 2, 3},
+                                /* repeatIndex= */ -1))
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP))
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
+                        .compose()));
+    }
+
+    @Test
+    public void areVibrationFeaturesSupported_composedEffects_someComponentsUnupported() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP)
+                .build();
+
+        // Not supported due to the TICK primitive, which the vibrator has no support for.
+        assertFalse(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
+                        .addEffect(VibrationEffect.createWaveform(
+                                /* timings= */ new long[] {1, 2, 3},
+                                /* amplitudes= */ new int[] {10, 20, 255},
+                                /* repeatIndex= */ -1))
+                        .compose()));
+        // Not supported due to the THUD effect, which the vibrator has no support for.
+        assertFalse(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                        .addEffect(VibrationEffect.createWaveform(
+                                /* timings= */ new long[] {1, 2, 3},
+                                /* amplitudes= */ new int[] {10, 20, 255},
+                                /* repeatIndex= */ -1))
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_THUD))
+                        .compose()));
+
+        info = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10)
+                .setSupportedEffects(VibrationEffect.EFFECT_POP)
+                .build();
+
+        // Not supported due to fractional amplitudes (amplitudes not min (0) or max (255) or
+        // DEFAULT), because the vibrator has no amplitude control.
+        assertFalse(info.areVibrationFeaturesSupported(
+                VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD)
+                        .addEffect(VibrationEffect.createWaveform(
+                                /* timings= */ new long[] {1, 2, 3},
+                                /* amplitudes= */ new int[] {10, 20, 255},
+                                /* repeatIndex= */ -1))
+                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP))
+                        .compose()));
+    }
+
+    private static VibrationEffect waveformWithAmplitudes(int...amplitudes) {
+        long[] timings = new long[amplitudes.length];
+        for (int i = 0; i < timings.length; i++) {
+            timings[i] = i * 2; // Arbitrary timings.
+        }
+        return VibrationEffect.createWaveform(timings, amplitudes, /* repeatIndex= */ -1);
+    }
 }
diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java
index c559e34..cfa12bb 100644
--- a/core/tests/vibrator/src/android/os/VibratorTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorTest.java
@@ -37,7 +37,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.hardware.vibrator.IVibrator;
 import android.media.AudioAttributes;
 import android.os.test.TestLooper;
 
@@ -60,8 +59,6 @@
     @Rule
     public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
 
-    private static final float TEST_TOLERANCE = 1e-5f;
-
     private Context mContextSpy;
     private Vibrator mVibratorSpy;
     private TestLooper mTestLooper;
@@ -79,9 +76,6 @@
     @Test
     public void getId_returnsDefaultId() {
         assertEquals(-1, mVibratorSpy.getId());
-        assertEquals(-1, new SystemVibrator.NoVibratorInfo().getId());
-        assertEquals(-1, new SystemVibrator.MultiVibratorInfo(new VibratorInfo[] {
-                VibratorInfo.EMPTY_VIBRATOR_INFO, VibratorInfo.EMPTY_VIBRATOR_INFO }).getId());
     }
 
     @Test
@@ -95,53 +89,6 @@
     }
 
     @Test
-    public void areEffectsSupported_noVibrator_returnsAlwaysNo() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
-    public void areEffectsSupported_unsupportedInOneVibrator_returnsNo() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo unsupportedVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setSupportedEffects(new int[0])
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
-    public void areEffectsSupported_unknownInOneVibrator_returnsUnknown() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo unknownSupportVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unknownSupportVibrator});
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
-    public void arePrimitivesSupported_supportedInAllVibrators_returnsYes() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_YES,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
     public void arePrimitivesSupported_returnsArrayOfSameSize() {
         assertEquals(0, mVibratorSpy.arePrimitivesSupported(new int[0]).length);
         assertEquals(1, mVibratorSpy.arePrimitivesSupported(
@@ -152,39 +99,6 @@
     }
 
     @Test
-    public void arePrimitivesSupported_noVibrator_returnsAlwaysFalse() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-        assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void arePrimitivesSupported_unsupportedInOneVibrator_returnsFalse() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build();
-        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
-        assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void arePrimitivesSupported_supportedInAllVibrators_returnsTrue() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5)
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 15)
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-        assertTrue(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
     public void getPrimitivesDurations_returnsArrayOfSameSize() {
         assertEquals(0, mVibratorSpy.getPrimitiveDurations(new int[0]).length);
         assertEquals(1, mVibratorSpy.getPrimitiveDurations(
@@ -195,245 +109,6 @@
     }
 
     @Test
-    public void getPrimitivesDurations_noVibrator_returnsAlwaysZero() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-        assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void getPrimitivesDurations_unsupportedInOneVibrator_returnsZero() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build();
-        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
-        assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void getPrimitivesDurations_supportedInAllVibrators_returnsMaxDuration() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-        assertEquals(20, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void getQFactorAndResonantFrequency_noVibrator_returnsNaN() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-
-        assertTrue(Float.isNaN(info.getQFactor()));
-        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
-    }
-
-    @Test
-    public void getQFactorAndResonantFrequency_differentValues_returnsNaN() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(1f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(2f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-
-        assertTrue(Float.isNaN(info.getQFactor()));
-        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
-        assertEmptyFrequencyProfileAndControl(info);
-
-        // One vibrator with values undefined.
-        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, thirdVibrator});
-
-        assertTrue(Float.isNaN(info.getQFactor()));
-        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getQFactorAndResonantFrequency_sameValues_returnsValue() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(10f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
-                        /* resonantFrequencyHz= */ 11, 10, 0.5f, null))
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(10f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
-                        /* resonantFrequencyHz= */ 11, 5, 1, null))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-
-        assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
-        assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
-
-        // No frequency range defined.
-        assertTrue(info.getFrequencyProfile().isEmpty());
-        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-    }
-
-    @Test
-    public void getFrequencyProfile_noVibrator_returnsEmpty() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_differentResonantFrequencyOrResolutionValues_returnsEmpty() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, differentResonantFrequency});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
-                        new float[] { 0, 1 }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, differentFrequencyResolution});
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_missingValues_returnsEmpty() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingResonantFrequency});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingMinFrequency});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
-                        new float[] { 0, 1 }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingFrequencyResolution});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingMaxAmplitudes});
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
-                        new float[] { 0, 1, 1, 0 }))
-                .build();
-        VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
-                        new float[] { 0, 1, 1, 0 }))
-                .build();
-        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 0, 1, 1, 0 }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, unalignedMinFrequency, thirdVibrator});
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_alignedProfiles_returnsIntersection() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
-                        new float[] { 0.5f, 1, 1, 0.5f }))
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 1, 1, 1 }))
-                .build();
-        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
-
-        assertEquals(
-                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
-                info.getFrequencyProfile());
-        assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-
-        // Third vibrator without frequency control capability.
-        thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
-
-        assertEquals(
-                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
-                info.getFrequencyProfile());
-        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-    }
-
-    @Test
     public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() {
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
         when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]);
@@ -577,212 +252,4 @@
         VibrationAttributes vibrationAttributes = captor.getValue();
         assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes);
     }
-
-    @Test
-    public void areVibrationFeaturesSupported_noAmplitudeControl_fractionalAmplitudes() {
-        Vibrator vibrator =
-                createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                        .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                        .setSupportedEffects(VibrationEffect.EFFECT_THUD)
-                        .build());
-
-        // Have at least one fractional amplitude (amplitude not min (0) or max (255) or DEFAULT).
-        assertFalse(vibrator.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30)));
-        assertFalse(vibrator.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 255)));
-        assertFalse(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createOneShot(20, /* amplitude= */ 40)));
-    }
-
-    @Test
-    public void areVibrationFeaturesSupported_noAmplitudeControl_nonFractionalAmplitudes() {
-        Vibrator vibrator =
-                createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                        .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                        .setSupportedEffects(VibrationEffect.EFFECT_THUD)
-                        .build());
-
-        // All amplitudes are min, max, or default. Requires no amplitude control.
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createWaveform(
-                        /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createOneShot(20, /* amplitude= */ 255)));
-    }
-
-    @Test
-    public void areVibrationFeaturesSupported_withAmplitudeControl() {
-        Vibrator vibrator =
-                createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                        .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
-                        .build());
-
-        // All forms of amplitudes are valid when amplitude control is available.
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createWaveform(
-                        /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30, 50)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                waveformWithAmplitudes(7, 255, 0, 0, 60)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createOneShot(20, /* amplitude= */ 255)));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.createOneShot(20, /* amplitude= */ 40)));
-    }
-
-    @Test
-    public void areVibrationFeaturesSupported_primitiveCompositionsWithSupportedPrimitives() {
-        Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build());
-
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                        .compose()));
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(
-                                VibrationEffect.Composition.PRIMITIVE_CLICK,
-                                /* scale= */ 0.2f,
-                                /* delay= */ 200)
-                        .compose()));
-    }
-
-    @Test
-    public void areVibrationFeaturesSupported_primitiveCompositionsWithUnupportedPrimitives() {
-        Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build());
-
-        assertFalse(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD)
-                        .compose()));
-        assertFalse(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)
-                        .compose()));
-    }
-
-    @Test
-    public void areVibrationFeaturesSupported_composedEffects_allComponentsSupported() {
-        Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP)
-                .build());
-
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                        .addEffect(VibrationEffect.createWaveform(
-                                /* timings= */ new long[] {1, 2, 3},
-                                /* amplitudes= */ new int[] {10, 20, 255},
-                                /* repeatIndex= */ -1))
-                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
-                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP))
-                        .compose()));
-
-        vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10)
-                .setSupportedEffects(VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_CLICK)
-                .build());
-
-        assertTrue(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD)
-                        .addEffect(VibrationEffect.createWaveform(
-                                // These timings are given either 0 or default amplitudes, which
-                                // do not require vibrator's amplitude control.
-                                /* timings= */ new long[] {1, 2, 3},
-                                /* repeatIndex= */ -1))
-                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP))
-                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
-                        .compose()));
-    }
-
-    @Test
-    public void areVibrationFeaturesSupported_composedEffects_someComponentsUnupported() {
-        Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP)
-                .build());
-
-        // Not supported due to the TICK primitive, which the vibrator has no support for.
-        assertFalse(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
-                        .addEffect(VibrationEffect.createWaveform(
-                                /* timings= */ new long[] {1, 2, 3},
-                                /* amplitudes= */ new int[] {10, 20, 255},
-                                /* repeatIndex= */ -1))
-                        .compose()));
-        // Not supported due to the THUD effect, which the vibrator has no support for.
-        assertFalse(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                        .addEffect(VibrationEffect.createWaveform(
-                                /* timings= */ new long[] {1, 2, 3},
-                                /* amplitudes= */ new int[] {10, 20, 255},
-                                /* repeatIndex= */ -1))
-                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_THUD))
-                        .compose()));
-
-        vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10)
-                .setSupportedEffects(VibrationEffect.EFFECT_POP)
-                .build());
-
-        // Not supported due to fractional amplitudes (amplitudes not min (0) or max (255) or
-        // DEFAULT), because the vibrator has no amplitude control.
-        assertFalse(vibrator.areVibrationFeaturesSupported(
-                VibrationEffect.startComposition()
-                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD)
-                        .addEffect(VibrationEffect.createWaveform(
-                                /* timings= */ new long[] {1, 2, 3},
-                                /* amplitudes= */ new int[] {10, 20, 255},
-                                /* repeatIndex= */ -1))
-                        .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP))
-                        .compose()));
-    }
-
-    /**
-     * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
-     */
-    void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
-        assertTrue(info.getFrequencyProfile().isEmpty());
-        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-    }
-
-    private Vibrator createVibratorWithCustomInfo(VibratorInfo info) {
-        return new SystemVibrator(mContextSpy) {
-            @Override
-            public VibratorInfo getInfo() {
-                return info;
-            }
-        };
-    }
-
-    private static VibrationEffect waveformWithAmplitudes(int...amplitudes) {
-        long[] timings = new long[amplitudes.length];
-        for (int i = 0; i < timings.length; i++) {
-            timings[i] = i * 2; // Arbitrary timings.
-        }
-        return VibrationEffect.createWaveform(timings, amplitudes, /* repeatIndex= */ -1);
-    }
 }
diff --git a/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java
new file mode 100644
index 0000000..fc31ac4
--- /dev/null
+++ b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2023 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 junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.TestCase.assertEquals;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.VibratorInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MultiVibratorInfoTest {
+    private static final float TEST_TOLERANCE = 1e-5f;
+
+    @Test
+    public void testGetId() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setSupportedEffects(new int[0])
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 3,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(3, info.getId());
+    }
+
+    @Test
+    public void testIsEffectSupported_supportedInAllVibrators_returnsYes() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK)
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_YES,
+                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
+    }
+
+    @Test
+    public void testIsEffectSupported_unsupportedInOneVibrator_returnsNo() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo unsupportedVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setSupportedEffects(new int[0])
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
+
+        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO,
+                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
+    }
+
+    @Test
+    public void testIsEffectSupported_unknownInOneVibrator_returnsUnknown() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo unknownSupportVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unknownSupportVibrator});
+        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN,
+                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
+    }
+
+    @Test
+    public void testIsPrimitiveSupported_unsupportedInOneVibrator_returnsFalse() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
+
+        assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testIsPrimitiveSupported_supportedInAllVibrators_returnsTrue() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 15)
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertTrue(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testGetPrimitiveDuration_unsupportedInOneVibrator_returnsZero() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
+
+        assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testGetPrimitiveDuration_supportedInAllVibrators_returnsMaxDuration() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(20, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testGetQFactorAndResonantFrequency_differentValues_returnsNaN() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(1f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(2f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertTrue(Float.isNaN(info.getQFactor()));
+        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
+
+        // One vibrator with values undefined.
+        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, thirdVibrator});
+
+        assertTrue(Float.isNaN(info.getQFactor()));
+        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetQFactorAndResonantFrequency_sameValues_returnsValue() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(10f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
+                        /* resonantFrequencyHz= */ 11, 10, 0.5f, null))
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(10f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
+                        /* resonantFrequencyHz= */ 11, 5, 1, null))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
+        assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
+        // No frequency range defined.
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
+
+    @Test
+    public void testGetFrequencyProfile_differentResonantFrequencyOrResolutions_returnsEmpty() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, differentResonantFrequency});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
+                        new float[] { 0, 1 }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, differentFrequencyResolution});
+
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetFrequencyProfile_missingValues_returnsEmpty() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingResonantFrequency});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingMinFrequency});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
+                        new float[] { 0, 1 }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingFrequencyResolution});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingMaxAmplitudes});
+
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
+                        new float[] { 0, 1, 1, 0 }))
+                .build();
+        VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
+                        new float[] { 0, 1, 1, 0 }))
+                .build();
+        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0, 1, 1, 0 }))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, unalignedMinFrequency, thirdVibrator});
+
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetFrequencyProfile_alignedProfiles_returnsIntersection() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
+                        new float[] { 0.5f, 1, 1, 0.5f }))
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 1, 1, 1 }))
+                .build();
+        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo, thirdVibrator});
+
+        assertEquals(
+                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+                info.getFrequencyProfile());
+        assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+
+        // Third vibrator without frequency control capability.
+        thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo, thirdVibrator});
+
+        assertEquals(
+                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+                info.getFrequencyProfile());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
+
+    /**
+     * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
+     */
+    private void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
+}
diff --git a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java
index 8268077..4f5f3c0 100644
--- a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java
@@ -25,18 +25,14 @@
 import static org.testng.Assert.assertThrows;
 
 import android.os.Parcel;
-import android.os.SystemVibrator;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
 import android.os.VibratorInfo;
 
-import androidx.test.InstrumentationRegistry;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.junit.runners.JUnit4;
 
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(JUnit4.class)
 public class PrebakedSegmentTest {
 
     @Test
@@ -149,121 +145,121 @@
 
     @Test
     public void testVibrationFeaturesSupport_idsWithFallback_fallbackEnabled_vibratorSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(
                 VibrationEffect.EFFECT_TICK,
                 VibrationEffect.EFFECT_CLICK,
                 VibrationEffect.EFFECT_DOUBLE_CLICK,
                 VibrationEffect.EFFECT_HEAVY_CLICK);
 
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
 
     }
 
     @Test
     public void testVibrationFeaturesSupport_idsWithFallback_fallbackEnabled_noVibratorSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]);
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]);
 
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_idsWithFallback_fallbackDisabled_vibratorSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(
                 VibrationEffect.EFFECT_TICK,
                 VibrationEffect.EFFECT_CLICK,
                 VibrationEffect.EFFECT_DOUBLE_CLICK,
                 VibrationEffect.EFFECT_HEAVY_CLICK);
 
         assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_HEAVY_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_idsWithFallback_fallbackDisabled_noVibratorSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]);
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]);
 
         assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_DOUBLE_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_HEAVY_CLICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackEnabled_vibratorSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(
                 VibrationEffect.EFFECT_THUD,
                 VibrationEffect.EFFECT_POP,
                 VibrationEffect.EFFECT_TEXTURE_TICK);
 
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_THUD)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_POP)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackEnabled_noVibratorSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]);
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]);
 
         assertFalse(createSegmentWithFallback(VibrationEffect.EFFECT_THUD)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertFalse(createSegmentWithFallback(VibrationEffect.EFFECT_POP)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertFalse(createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackDisabled_vibratorSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(
                 VibrationEffect.EFFECT_THUD,
                 VibrationEffect.EFFECT_POP,
                 VibrationEffect.EFFECT_TEXTURE_TICK);
 
         assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_THUD)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_POP)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_TEXTURE_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackDisabled_noVibSupport() {
-        Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]);
+        VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]);
 
         assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_THUD)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_POP)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
         assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_TEXTURE_TICK)
-                .areVibrationFeaturesSupported(vibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
@@ -283,14 +279,9 @@
         return new PrebakedSegment(effectId, false, VibrationEffect.EFFECT_STRENGTH_MEDIUM);
     }
 
-    private static Vibrator createVibratorWithSupportedEffects(int... supportedEffects) {
-        return new SystemVibrator(InstrumentationRegistry.getContext()) {
-            @Override
-            public VibratorInfo getInfo() {
-                return new VibratorInfo.Builder(/* id= */ 1)
-                        .setSupportedEffects(supportedEffects)
-                        .build();
-            }
-        };
+    private static VibratorInfo createVibratorInfoWithSupportedEffects(int... supportedEffects) {
+        return new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(supportedEffects)
+                .build();
     }
 }
diff --git a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
index 6f5adcd..ec5a084 100644
--- a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
@@ -25,18 +25,14 @@
 
 import android.hardware.vibrator.IVibrator;
 import android.os.Parcel;
-import android.os.SystemVibrator;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
 import android.os.VibratorInfo;
 
-import androidx.test.InstrumentationRegistry;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.junit.runners.JUnit4;
 
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(JUnit4.class)
 public class PrimitiveSegmentTest {
     private static final float TOLERANCE = 1e-2f;
 
@@ -146,15 +142,15 @@
     public void testVibrationFeaturesSupport_primitiveSupportedByVibrator() {
         assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_CLICK)
                 .areVibrationFeaturesSupported(
-                        createVibratorWithSupportedPrimitive(
+                        createVibratorInfoWithSupportedPrimitive(
                                 VibrationEffect.Composition.PRIMITIVE_CLICK)));
         assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_THUD)
                 .areVibrationFeaturesSupported(
-                        createVibratorWithSupportedPrimitive(
+                        createVibratorInfoWithSupportedPrimitive(
                                 VibrationEffect.Composition.PRIMITIVE_THUD)));
         assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE)
                 .areVibrationFeaturesSupported(
-                        createVibratorWithSupportedPrimitive(
+                        createVibratorInfoWithSupportedPrimitive(
                                 VibrationEffect.Composition.PRIMITIVE_QUICK_RISE)));
     }
 
@@ -162,15 +158,15 @@
     public void testVibrationFeaturesSupport_primitiveNotSupportedByVibrator() {
         assertFalse(createSegment(VibrationEffect.Composition.PRIMITIVE_CLICK)
                 .areVibrationFeaturesSupported(
-                        createVibratorWithSupportedPrimitive(
+                        createVibratorInfoWithSupportedPrimitive(
                                 VibrationEffect.Composition.PRIMITIVE_THUD)));
         assertFalse(createSegment(VibrationEffect.Composition.PRIMITIVE_THUD)
                 .areVibrationFeaturesSupported(
-                        createVibratorWithSupportedPrimitive(
+                        createVibratorInfoWithSupportedPrimitive(
                                 VibrationEffect.Composition.PRIMITIVE_CLICK)));
         assertFalse(createSegment(VibrationEffect.Composition.PRIMITIVE_THUD)
                 .areVibrationFeaturesSupported(
-                        createVibratorWithSupportedPrimitive(
+                        createVibratorInfoWithSupportedPrimitive(
                                 VibrationEffect.Composition.PRIMITIVE_QUICK_RISE)));
     }
 
@@ -193,15 +189,10 @@
         return new PrimitiveSegment(primitiveId, 0.2f, 10);
     }
 
-    private static Vibrator createVibratorWithSupportedPrimitive(int primitiveId) {
-        return new SystemVibrator(InstrumentationRegistry.getContext()) {
-            @Override
-            public VibratorInfo getInfo() {
-                return new VibratorInfo.Builder(/* id= */ 1)
-                        .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                        .setSupportedPrimitive(primitiveId, 10)
-                        .build();
-                }
-        };
+    private static VibratorInfo createVibratorInfoWithSupportedPrimitive(int primitiveId) {
+        return new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(primitiveId, 10)
+                .build();
     }
 }
diff --git a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
index 68870e5..5caa86b 100644
--- a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
@@ -23,31 +23,21 @@
 import static junit.framework.Assert.assertSame;
 import static junit.framework.Assert.assertTrue;
 
-import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 
+import android.hardware.vibrator.IVibrator;
 import android.os.Parcel;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
+import android.os.VibratorInfo;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.mockito.junit.MockitoRule;
+import org.junit.runners.JUnit4;
 
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(JUnit4.class)
 public class RampSegmentTest {
     private static final float TOLERANCE = 1e-2f;
 
-    @Rule
-    public MockitoRule mMockitoRule = MockitoJUnit.rule();
-
-    @Mock
-    private Vibrator mVibrator;
-
     @Test
     public void testCreation() {
         RampSegment ramp = new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0,
@@ -147,71 +137,71 @@
 
     @Test
     public void testVibrationFeaturesSupport_amplitudeAndFrequencyControls_supported() {
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true);
 
         // Increasing amplitude
-        assertTrue(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(mVibrator));
+        assertTrue(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(info));
         // Increasing frequency
-        assertTrue(new RampSegment(0.5f, 0.5f, 0, 1, 10).areVibrationFeaturesSupported(mVibrator));
+        assertTrue(new RampSegment(0.5f, 0.5f, 0, 1, 10).areVibrationFeaturesSupported(info));
         // Decreasing amplitude
-        assertTrue(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator));
+        assertTrue(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(info));
         // Decreasing frequency
-        assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 10).areVibrationFeaturesSupported(mVibrator));
+        assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 10).areVibrationFeaturesSupported(info));
         // Zero duration
-        assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 0).areVibrationFeaturesSupported(mVibrator));
+        assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 0).areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_noAmplitudeControl_unsupportedForChangingAmplitude() {
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true);
 
         // Test with increasing/decreasing amplitudes.
-        assertFalse(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(mVibrator));
-        assertFalse(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator));
+        assertFalse(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(info));
+        assertFalse(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_noAmplitudeControl_fractionalAmplitudeUnsupported() {
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true);
 
-        assertFalse(new RampSegment(0.2f, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator));
-        assertFalse(new RampSegment(0, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator));
-        assertFalse(new RampSegment(0.2f, 0, 0, 0, 10).areVibrationFeaturesSupported(mVibrator));
+        assertFalse(new RampSegment(0.2f, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(info));
+        assertFalse(new RampSegment(0, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(info));
+        assertFalse(new RampSegment(0.2f, 0, 0, 0, 10).areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_unchangingZeroAmplitude_supported() {
         RampSegment amplitudeZeroWithIncreasingFrequency = new RampSegment(1, 1, 0.5f, 0.8f, 10);
         RampSegment amplitudeZeroWithDecreasingFrequency = new RampSegment(1, 1, 0.8f, 0.5f, 10);
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true);
 
-        assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(info));
+        assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
+        info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true);
 
-        assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(info));
+        assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_unchangingOneAmplitude_supported() {
         RampSegment amplitudeOneWithIncreasingFrequency = new RampSegment(1, 1, 0.5f, 0.8f, 10);
         RampSegment amplitudeOneWithDecreasingFrequency = new RampSegment(1, 1, 0.8f, 0.5f, 10);
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true);
 
-        assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(info));
+        assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
+        info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true);
 
-        assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(info));
+        assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(info));
     }
 
     @Test
@@ -220,52 +210,52 @@
                 new RampSegment(DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE, 0.5f, 0.8f, 10);
         RampSegment defaultAmplitudeDecreasingFrequency =
                 new RampSegment(DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE, 0.8f, 0.5f, 10);
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true);
 
-        assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(info));
+        assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
+        info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true);
 
-        assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(info));
+        assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_noFrequencyControl_unsupportedForChangingFrequency() {
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
-        when(mVibrator.hasFrequencyControl()).thenReturn(false);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ false);
 
         // Test with increasing/decreasing frequencies.
-        assertFalse(new RampSegment(0, 0, 0.2f, 0.4f, 10).areVibrationFeaturesSupported(mVibrator));
-        assertFalse(new RampSegment(0, 0, 0.4f, 0.2f, 10).areVibrationFeaturesSupported(mVibrator));
+        assertFalse(new RampSegment(0, 0, 0.2f, 0.4f, 10).areVibrationFeaturesSupported(info));
+        assertFalse(new RampSegment(0, 0, 0.4f, 0.2f, 10).areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_noFrequencyControl_fractionalFrequencyUnsupported() {
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
-        when(mVibrator.hasFrequencyControl()).thenReturn(false);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ false);
 
-        assertFalse(new RampSegment(0, 0, 0.2f, 0.2f, 10).areVibrationFeaturesSupported(mVibrator));
-        assertFalse(new RampSegment(0, 0, 0.2f, 0, 10).areVibrationFeaturesSupported(mVibrator));
-        assertFalse(new RampSegment(0, 0, 0, 0.2f, 10).areVibrationFeaturesSupported(mVibrator));
+        assertFalse(new RampSegment(0, 0, 0.2f, 0.2f, 10).areVibrationFeaturesSupported(info));
+        assertFalse(new RampSegment(0, 0, 0.2f, 0, 10).areVibrationFeaturesSupported(info));
+        assertFalse(new RampSegment(0, 0, 0, 0.2f, 10).areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_unchangingZeroFrequency_supported() {
         RampSegment frequencyZeroWithIncreasingAmplitude = new RampSegment(0.1f, 1, 0, 0, 10);
         RampSegment frequencyZeroWithDecreasingAmplitude = new RampSegment(1, 0.1f, 0, 0, 10);
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
-        when(mVibrator.hasFrequencyControl()).thenReturn(false);
+        VibratorInfo info =
+                createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ false);
 
-        assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(info));
+        assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true);
 
-        assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(mVibrator));
-        assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(info));
+        assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(info));
     }
 
     @Test
@@ -274,4 +264,17 @@
         // duration checked in VibrationEffect implementations.
         assertTrue(new RampSegment(0.5f, 1, 0, 0, 5_000).isHapticFeedbackCandidate());
     }
+
+    private static VibratorInfo createVibInfo(
+            boolean hasAmplitudeControl, boolean hasFrequencyControl) {
+        VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1);
+        long capabilities = 0;
+        if (hasAmplitudeControl) {
+            capabilities |= IVibrator.CAP_AMPLITUDE_CONTROL;
+        }
+        if (hasFrequencyControl) {
+            capabilities |= (IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
+        }
+        return builder.setCapabilities(capabilities).build();
+    }
 }
diff --git a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
index 34bb892..44db306 100644
--- a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
@@ -21,31 +21,20 @@
 import static junit.framework.Assert.assertSame;
 import static junit.framework.Assert.assertTrue;
 
-import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 
+import android.hardware.vibrator.IVibrator;
 import android.os.Parcel;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
+import android.os.VibratorInfo;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.mockito.junit.MockitoRule;
+import org.junit.runners.JUnit4;
 
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(JUnit4.class)
 public class StepSegmentTest {
     private static final float TOLERANCE = 1e-2f;
-
-    @Rule
-    public MockitoRule mMockitoRule = MockitoJUnit.rule();
-
-    @Mock
-    private Vibrator mVibrator;
-
     @Test
     public void testCreation() {
         StepSegment step = new StepSegment(/* amplitude= */ 1f, /* frequencyHz= */ 1f,
@@ -160,26 +149,26 @@
     public void testVibrationFeaturesSupport_zeroAmplitude_supported() {
         StepSegment segment =
                 new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 0, /* duration= */ 0);
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
+        VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
+        info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_maxAmplitude_supported() {
         StepSegment segment =
                 new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 0, /* duration= */ 0);
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
+        VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
+        info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
     }
 
     @Test
@@ -189,60 +178,60 @@
                         /* amplitude= */ VibrationEffect.DEFAULT_AMPLITUDE,
                         /* frequencyHz= */ 0,
                         /* duration= */ 0);
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
+        VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
+        info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_fractionalAmplitude_hasAmplitudeCtrl_supported() {
-        when(mVibrator.hasAmplitudeControl()).thenReturn(true);
+        VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true);
 
         assertTrue(new StepSegment(/* amplitude= */ 0.2f, /* frequencyHz= */ 0, /* duration= */ 0)
-                .areVibrationFeaturesSupported(mVibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_fractionalAmplitude_hasNoAmplitudeCtrl_notSupported() {
-        when(mVibrator.hasAmplitudeControl()).thenReturn(false);
+        VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false);
 
         assertFalse(new StepSegment(/* amplitude= */ 0.2f, /* frequencyHz= */ 0, /* duration= */ 0)
-                .areVibrationFeaturesSupported(mVibrator));
+                .areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_zeroFrequency_supported() {
         StepSegment segment =
                 new StepSegment(/* amplitude= */ 0f, /* frequencyHz= */ 0, /* duration= */ 0);
-        when(mVibrator.hasFrequencyControl()).thenReturn(false);
+        VibratorInfo info = createVibInfoForFrequency(/* hasFrequencyControl= */ false);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
 
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        info = createVibInfoForFrequency(/* hasFrequencyControl= */ true);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_nonZeroFrequency_hasFrequencyCtrl_supported() {
         StepSegment segment =
                 new StepSegment(/* amplitude= */ 0f, /* frequencyHz= */ 0.2f, /* duration= */ 0);
-        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        VibratorInfo info = createVibInfoForFrequency(/* hasFrequencyControl= */ true);
 
-        assertTrue(segment.areVibrationFeaturesSupported(mVibrator));
+        assertTrue(segment.areVibrationFeaturesSupported(info));
     }
 
     @Test
     public void testVibrationFeaturesSupport_nonZeroFrequency_hasNoFrequencyCtrl_notSupported() {
         StepSegment segment =
                 new StepSegment(/* amplitude= */ 0f, /* frequencyHz= */ 0.2f, /* duration= */ 0);
-        when(mVibrator.hasFrequencyControl()).thenReturn(false);
+        VibratorInfo info = createVibInfoForFrequency(/* hasFrequencyControl= */ false);
 
-        assertFalse(segment.areVibrationFeaturesSupported(mVibrator));
+        assertFalse(segment.areVibrationFeaturesSupported(info));
     }
 
     @Test
@@ -251,4 +240,21 @@
         // duration checked in VibrationEffect implementations.
         assertTrue(new StepSegment(0, 0, 5_000).isHapticFeedbackCandidate());
     }
+
+    private static VibratorInfo createVibInfoForAmplitude(boolean hasAmplitudeControl) {
+        VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1);
+        if (hasAmplitudeControl) {
+            builder.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        }
+        return builder.build();
+    }
+
+    private static VibratorInfo createVibInfoForFrequency(boolean hasFrequencyControl) {
+        VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1);
+        if (hasFrequencyControl) {
+            builder.setCapabilities(
+                    IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
+        }
+        return builder.build();
+    }
 }
diff --git a/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java b/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java
new file mode 100644
index 0000000..df4822f
--- /dev/null
+++ b/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2023 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 junit.framework.Assert.assertTrue;
+import static junit.framework.TestCase.assertEquals;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibrationEffect;
+import android.os.VibratorInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class VibratorInfoFactoryTest {
+
+    @Test
+    public void testCreatedInfo_hasTheRequestedId() {
+        // Empty info list.
+        VibratorInfo infoFromEmptyInfos =
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {});
+        VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo infoFromOneInfo =
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info1});
+        VibratorInfo infoFromTwoInfos =
+                VibratorInfoFactory.create(/* id= */ -3, new VibratorInfo[] {info1, info2});
+
+        assertEquals(3, infoFromEmptyInfos.getId());
+        assertEquals(-1, infoFromOneInfo.getId());
+        assertEquals(-3, infoFromTwoInfos.getId());
+    }
+
+    @Test
+    public void testCreatedInfo_fromEmptyVibratorInfos_returnsEmptyVibratorInfo() {
+        VibratorInfo info = VibratorInfoFactory.create(/* id= */ 2, new VibratorInfo[] {});
+
+        assertEqualContent(VibratorInfo.EMPTY_VIBRATOR_INFO, info);
+    }
+
+    @Test
+    public void testCreatedInfo_fromSingleVibratorInfo_hasEqualContent() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_FREQUENCY_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_THUD)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 30)
+                .build();
+
+        VibratorInfo createdInfo =
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info});
+
+        assertEqualContent(info, createdInfo);
+    }
+
+    @Test
+    public void testCreatedInfo_hasEqualContentRegardlessOfSourceInfoOrder() {
+        VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+
+        assertEqualContent(
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info1, info2}),
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info2, info1}));
+    }
+
+    @Test
+    public void testCreatedInfoContents() {
+        VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ -1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_FREQUENCY_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_POP)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 5)
+                .build();
+        VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ -2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_THUD)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 20)
+                .build();
+        VibratorInfo info3 = new VibratorInfo.Builder(/* id= */ -3)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build();
+
+        assertEquals(
+                new VibratorInfo.Builder(/* id= */ 3)
+                        .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                        .setSupportedEffects(VibrationEffect.EFFECT_POP)
+                        .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 20)
+                        .build(),
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info1, info2}));
+        assertEquals(
+                new VibratorInfo.Builder(/* id= */ 3)
+                        .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                        .build(),
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info2, info3}));
+        assertEquals(
+                new VibratorInfo.Builder(/* id= */ 3).build(),
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info1, info3}));
+    }
+
+    private static void assertEqualContent(VibratorInfo info1, VibratorInfo info2) {
+        assertTrue(info1.equalContent(info2));
+    }
+}
diff --git a/graphics/java/android/graphics/Gainmap.java b/graphics/java/android/graphics/Gainmap.java
index f639521..b5fb13d 100644
--- a/graphics/java/android/graphics/Gainmap.java
+++ b/graphics/java/android/graphics/Gainmap.java
@@ -124,8 +124,6 @@
     /**
      * Creates a new gainmap using the provided gainmap as the metadata source and the provided
      * bitmap as the replacement for the gainmapContents
-     * TODO: Make public, it's useful
-     * @hide
      */
     public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) {
         this(gainmapContents, nCreateCopy(gainmap.mNativePtr));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
index ef93a33..be1b9b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
@@ -16,6 +16,7 @@
 package com.android.wm.shell.common.split;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -60,7 +61,8 @@
     public static final int[] CONTROLLED_WINDOWING_MODES =
             {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
     public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
-            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
+            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW,
+            WINDOWING_MODE_FREEFORM};
 
     /** Flag applied to a transition change to identify it as a divider bar for animation. */
     public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 1d46e75..633f627 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -18,6 +18,7 @@
 
 import android.R
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -301,6 +302,24 @@
         }
     }
 
+    /** Move a desktop app to split screen. */
+    fun moveToSplit(task: RunningTaskInfo) {
+        KtProtoLog.v(
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: moveToSplit taskId=%d",
+            task.taskId
+        )
+        val wct = WindowContainerTransaction()
+        wct.setWindowingMode(task.token, WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW)
+        wct.setBounds(task.token, null)
+        wct.setDensityDpi(task.token, getDefaultDensityDpi())
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+        } else {
+            shellTaskOrganizer.applyTransaction(wct)
+        }
+    }
+
     /**
      * The second part of the animated move to desktop transition, called after
      * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index b872267..f25110a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -123,14 +123,6 @@
     private static final long PIP_KEEP_CLEAR_AREAS_DELAY =
             SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200);
 
-    private boolean mEnablePipKeepClearAlgorithm =
-            SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true);
-
-    @VisibleForTesting
-    void setEnablePipKeepClearAlgorithm(boolean value) {
-        mEnablePipKeepClearAlgorithm = value;
-    }
-
     private Context mContext;
     protected ShellExecutor mMainExecutor;
     private DisplayController mDisplayController;
@@ -166,10 +158,6 @@
             // early bail out if the change was caused by keyguard showing up
             return;
         }
-        if (!mEnablePipKeepClearAlgorithm) {
-            // early bail out if the keep clear areas feature is disabled
-            return;
-        }
         if (mPipBoundsState.isStashed()) {
             // don't move when stashed
             return;
@@ -187,10 +175,6 @@
     }
 
     private void updatePipPositionForKeepClearAreas() {
-        if (!mEnablePipKeepClearAlgorithm) {
-            // early bail out if the keep clear areas feature is disabled
-            return;
-        }
         if (mIsKeyguardShowingOrAnimating) {
             // early bail out if the change was caused by keyguard showing up
             return;
@@ -343,19 +327,17 @@
                 public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
                         Set<Rect> unrestricted) {
                     if (mPipDisplayLayoutState.getDisplayId() == displayId) {
-                        if (mEnablePipKeepClearAlgorithm) {
-                            mPipBoundsState.setKeepClearAreas(restricted, unrestricted);
+                        mPipBoundsState.setKeepClearAreas(restricted, unrestricted);
 
-                            mMainExecutor.removeCallbacks(
-                                    mMovePipInResponseToKeepClearAreasChangeCallback);
-                            mMainExecutor.executeDelayed(
-                                    mMovePipInResponseToKeepClearAreasChangeCallback,
-                                    PIP_KEEP_CLEAR_AREAS_DELAY);
+                        mMainExecutor.removeCallbacks(
+                                mMovePipInResponseToKeepClearAreasChangeCallback);
+                        mMainExecutor.executeDelayed(
+                                mMovePipInResponseToKeepClearAreasChangeCallback,
+                                PIP_KEEP_CLEAR_AREAS_DELAY);
 
-                            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                                    "onKeepClearAreasChanged: restricted=%s, unrestricted=%s",
-                                    restricted, unrestricted);
-                        }
+                        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                                "onKeepClearAreasChanged: restricted=%s, unrestricted=%s",
+                                restricted, unrestricted);
                     }
                 }
             };
@@ -660,25 +642,9 @@
                             // there's a keyguard present
                             return;
                         }
-                        int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
                         onDisplayChangedUncheck(mDisplayController
                                         .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()),
                                 false /* saveRestoreSnapFraction */);
-                        int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
-                        if (!mEnablePipKeepClearAlgorithm) {
-                            // offset PiP to adjust for bottom inset change
-                            int pipTop = mPipBoundsState.getBounds().top;
-                            int diff = newMaxMovementBound - oldMaxMovementBound;
-                            if (diff < 0 && pipTop > newMaxMovementBound) {
-                                // bottom inset has increased, move PiP up if it is too low
-                                mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(),
-                                        newMaxMovementBound - pipTop);
-                            }
-                            if (diff > 0 && oldMaxMovementBound == pipTop) {
-                                // bottom inset has decreased, move PiP down if it was by the edge
-                                mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), diff);
-                            }
-                        }
                     }
                 });
 
@@ -947,14 +913,8 @@
      * Sets both shelf visibility and its height.
      */
     private void setShelfHeight(boolean visible, int height) {
-        if (mEnablePipKeepClearAlgorithm) {
-            // turn this into Launcher keep clear area registration instead
-            setLauncherKeepClearAreaHeight(visible, height);
-            return;
-        }
-        if (!mIsKeyguardShowingOrAnimating) {
-            setShelfHeightLocked(visible, height);
-        }
+        // turn this into Launcher keep clear area registration instead
+        setLauncherKeepClearAreaHeight(visible, height);
     }
 
     private void setLauncherKeepClearAreaHeight(boolean visible, int height) {
@@ -1015,16 +975,10 @@
     private Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
             PictureInPictureParams pictureInPictureParams,
             int launcherRotation, Rect hotseatKeepClearArea) {
-
-        if (mEnablePipKeepClearAlgorithm) {
-            // preemptively add the keep clear area for Hotseat, so that it is taken into account
-            // when calculating the entry destination bounds of PiP window
-            mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG,
-                    hotseatKeepClearArea);
-        } else {
-            int shelfHeight = hotseatKeepClearArea.height();
-            setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight);
-        }
+        // preemptively add the keep clear area for Hotseat, so that it is taken into account
+        // when calculating the entry destination bounds of PiP window
+        mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG,
+                hotseatKeepClearArea);
         onDisplayRotationChangedNotInPip(mContext, launcherRotation);
         final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo,
                 pictureInPictureParams);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 3e95498a..9f7dee7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -34,7 +34,6 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.SystemProperties;
 import android.provider.DeviceConfig;
 import android.util.Size;
 import android.view.DisplayCutout;
@@ -73,14 +72,6 @@
     private static final String TAG = "PipTouchHandler";
     private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f;
 
-    private boolean mEnablePipKeepClearAlgorithm =
-            SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true);
-
-    @VisibleForTesting
-    void setEnablePipKeepClearAlgorithm(boolean value) {
-        mEnablePipKeepClearAlgorithm = value;
-    }
-
     // Allow PIP to resize to a slightly bigger state upon touch
     private boolean mEnableResize;
     private final Context mContext;
@@ -430,48 +421,6 @@
                 mIsImeShowing ? mImeOffset : 0,
                 !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0);
 
-        // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not
-        // occluded by the IME or shelf.
-        if (fromImeAdjustment || fromShelfAdjustment) {
-            if (mTouchState.isUserInteracting() && mTouchState.isDragging()) {
-                // Defer the update of the current movement bounds until after the user finishes
-                // touching the screen
-            } else if (mEnablePipKeepClearAlgorithm) {
-                // Ignore moving PiP if keep clear algorithm is enabled, since IME and shelf height
-                // now are accounted for in the keep clear algorithm calculations
-            } else {
-                final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu();
-                final Rect toMovementBounds = new Rect();
-                mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds,
-                        toMovementBounds, mIsImeShowing ? mImeHeight : 0);
-                final int prevBottom = mPipBoundsState.getMovementBounds().bottom
-                        - mMovementBoundsExtraOffsets;
-                // This is to handle landscape fullscreen IMEs, don't apply the extra offset in this
-                // case
-                final int toBottom = toMovementBounds.bottom < toMovementBounds.top
-                        ? toMovementBounds.bottom
-                        : toMovementBounds.bottom - extraOffset;
-
-                if (isExpanded) {
-                    curBounds.set(mPipBoundsState.getExpandedBounds());
-                    mPipBoundsAlgorithm.getSnapAlgorithm().applySnapFraction(curBounds,
-                            toMovementBounds, mSavedSnapFraction);
-                }
-
-                if (prevBottom < toBottom) {
-                    // The movement bounds are expanding
-                    if (curBounds.top > prevBottom - mBottomOffsetBufferPx) {
-                        mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top);
-                    }
-                } else if (prevBottom > toBottom) {
-                    // The movement bounds are shrinking
-                    if (curBounds.top > toBottom - mBottomOffsetBufferPx) {
-                        mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top);
-                    }
-                }
-            }
-        }
-
         // Update the movement bounds after doing the calculations based on the old movement bounds
         // above
         mPipBoundsState.setNormalMovementBounds(normalMovementBounds);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 2be7a49..29fff03 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -71,6 +71,7 @@
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellController;
@@ -200,6 +201,19 @@
     @Override
     public void setSplitScreenController(SplitScreenController splitScreenController) {
         mSplitScreenController = splitScreenController;
+        mSplitScreenController.registerSplitScreenListener(new SplitScreen.SplitScreenListener() {
+            @Override
+            public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+                if (visible) {
+                    DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId);
+                    if (decor != null && DesktopModeStatus.isActive(mContext)
+                            && decor.mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                        mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
+                        mDesktopTasksController.ifPresent(c -> c.moveToSplit(decor.mTaskInfo));
+                    }
+                }
+            }
+        });
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt
index 610cede..fa723e3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt
@@ -23,6 +23,7 @@
 import android.tools.common.NavBar
 import android.tools.common.Rotation
 import android.tools.device.apphelpers.MessagingAppHelper
+import android.tools.device.flicker.rules.ArtifactSaverRule
 import android.tools.device.flicker.rules.ChangeDisplayOrientationRule
 import android.tools.device.flicker.rules.LaunchAppRule
 import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
@@ -33,9 +34,10 @@
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
 
     fun testSetupRule(navigationMode: NavBar, rotation: Rotation): RuleChain {
-        return RuleChain.outerRule(UnlockScreenRule())
+        return RuleChain.outerRule(ArtifactSaverRule())
+            .around(UnlockScreenRule())
             .around(
-                NavigationModeRule(navigationMode.value, /* changeNavigationModeAfterTest */ false)
+                NavigationModeRule(navigationMode.value, false)
             )
             .around(
                 LaunchAppRule(MessagingAppHelper(instrumentation), clearCacheAfterParsing = false)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 0ae2908..05c6ba9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -76,7 +76,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Optional;
@@ -329,21 +328,7 @@
     }
 
     @Test
-    public void onKeepClearAreasChanged_featureDisabled_pipBoundsStateDoesntChange() {
-        mPipController.setEnablePipKeepClearAlgorithm(false);
-        final int displayId = 1;
-        final Rect keepClearArea = new Rect(0, 0, 10, 10);
-        when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId);
-
-        mPipController.mDisplaysChangedListener.onKeepClearAreasChanged(
-                displayId, Set.of(keepClearArea), Set.of());
-
-        verify(mMockPipBoundsState, never()).setKeepClearAreas(Mockito.anySet(), Mockito.anySet());
-    }
-
-    @Test
-    public void onKeepClearAreasChanged_featureEnabled_updatesPipBoundsState() {
-        mPipController.setEnablePipKeepClearAlgorithm(true);
+    public void onKeepClearAreasChanged_updatesPipBoundsState() {
         final int displayId = 1;
         final Rect keepClearArea = new Rect(0, 0, 10, 10);
         when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 852183c..f65d7af 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -18,7 +18,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -174,16 +173,4 @@
         verify(mPipResizeGestureHandler, times(1))
                 .updateMaxSize(expectedMaxSize.getWidth(), expectedMaxSize.getHeight());
     }
-
-    @Test
-    public void updateMovementBounds_withImeAdjustment_movesPip() {
-        mPipTouchHandler.setEnablePipKeepClearAlgorithm(false);
-        mFromImeAdjustment = true;
-        mPipTouchHandler.onImeVisibilityChanged(true /* imeVisible */, mImeHeight);
-
-        mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mPipBounds, mCurBounds,
-                mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation);
-
-        verify(mMotionHelper, times(1)).animateToOffset(any(), anyInt());
-    }
 }
diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h
index eb5878d..8c180da 100644
--- a/libs/hwui/DisplayList.h
+++ b/libs/hwui/DisplayList.h
@@ -54,6 +54,8 @@
         mImpl->updateChildren(std::move(updateFn));
     }
 
+    void visit(std::function<void(const RenderNode&)> func) const { mImpl->visit(std::move(func)); }
+
     [[nodiscard]] explicit operator bool() const {
         return mImpl.get() != nullptr;
     }
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 1dd22cf..eee93c1 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -109,6 +109,13 @@
     output << std::endl;
 }
 
+void RenderNode::visit(std::function<void(const RenderNode&)> func) const {
+    func(*this);
+    if (mDisplayList) {
+        mDisplayList.visit(func);
+    }
+}
+
 int RenderNode::getUsageSize() {
     int size = sizeof(RenderNode);
     size += mStagingDisplayList.getUsedSize();
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index d1e04ad..529a49e9 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -114,7 +114,7 @@
         return mDisplayList.containsProjectionReceiver();
     }
 
-    const char* getName() const { return mName.string(); }
+    const char* getName() const { return mName.c_str(); }
 
     void setName(const char* name) {
         if (name) {
@@ -129,10 +129,6 @@
 
     StretchMask& getStretchMask() { return mStretchMask; }
 
-    VirtualLightRefBase* getUserContext() const { return mUserContext.get(); }
-
-    void setUserContext(VirtualLightRefBase* context) { mUserContext = context; }
-
     bool isPropertyFieldDirty(DirtyPropertyMask field) const {
         return mDirtyPropertyFields & field;
     }
@@ -215,6 +211,8 @@
 
     void output(std::ostream& output, uint32_t level);
 
+    void visit(std::function<void(const RenderNode&)>) const;
+
     void setUsageHint(UsageHint usageHint) { mUsageHint = usageHint; }
 
     UsageHint usageHint() const { return mUsageHint; }
@@ -222,6 +220,7 @@
     int64_t uniqueId() const { return mUniqueId; }
 
     void setIsTextureView() { mIsTextureView = true; }
+    bool isTextureView() const { return mIsTextureView; }
 
     void markDrawStart(SkCanvas& canvas);
     void markDrawEnd(SkCanvas& canvas);
@@ -248,7 +247,6 @@
 
     const int64_t mUniqueId;
     String8 mName;
-    sp<VirtualLightRefBase> mUserContext;
 
     uint32_t mDirtyPropertyFields;
     RenderProperties mProperties;
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
index af2d3b3..5c8285a 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
@@ -66,6 +66,12 @@
     }
 }
 
+void SkiaDisplayList::visit(std::function<void(const RenderNode&)> func) const {
+    for (auto& child : mChildNodes) {
+        child.getRenderNode()->visit(func);
+    }
+}
+
 static bool intersects(const SkISize screenSize, const Matrix4& mat, const SkRect& bounds) {
     Vector3 points[] = { Vector3 {bounds.fLeft, bounds.fTop, 0},
                          Vector3 {bounds.fRight, bounds.fTop, 0},
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h
index 7af31a4..e5bd5c9 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.h
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h
@@ -145,6 +145,8 @@
      */
     void updateChildren(std::function<void(RenderNode*)> updateFn);
 
+    void visit(std::function<void(const RenderNode&)> func) const;
+
     /**
      *  Returns true if there is a child render node that is a projection receiver.
      */
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index 3d77877..6679f8f 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -201,7 +201,7 @@
             String8 cachesOutput;
             mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput,
                                                          &mRenderThread.renderState());
-            ALOGE("%s", cachesOutput.string());
+            ALOGE("%s", cachesOutput.c_str());
             if (errorHandler) {
                 std::ostringstream err;
                 err << "Unable to create layer for " << node->getName();
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 2e0de3f..4064bb9 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -1132,6 +1132,12 @@
     return self->mColorMode != ColorMode::Default;
 }
 
+void CanvasContext::visitAllRenderNodes(std::function<void(const RenderNode&)> func) const {
+    for (auto node : mRenderNodes) {
+        node->visit(func);
+    }
+}
+
 } /* namespace renderthread */
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 3f02674..241f8dd 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -238,6 +238,8 @@
 
     static bool shouldDither();
 
+    void visitAllRenderNodes(std::function<void(const RenderNode&)>) const;
+
 private:
     CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
                   IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline,
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index eb28c08..94ed06c 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -357,7 +357,15 @@
 
     String8 cachesOutput;
     mCacheManager->dumpMemoryUsage(cachesOutput, mRenderState);
-    dprintf(fd, "\nPipeline=%s\n%s\n", pipelineToString(), cachesOutput.string());
+    dprintf(fd, "\nPipeline=%s\n%s", pipelineToString(), cachesOutput.c_str());
+    for (auto&& context : mCacheManager->mCanvasContexts) {
+        context->visitAllRenderNodes([&](const RenderNode& node) {
+            if (node.isTextureView()) {
+                dprintf(fd, "TextureView: %dx%d\n", node.getWidth(), node.getHeight());
+            }
+        });
+    }
+    dprintf(fd, "\n");
 }
 
 void RenderThread::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) {
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index d778feb..d6b7ecf 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -208,3 +208,22 @@
     ],
     min_sdk_version: "24",
 }
+
+//##########################################################
+// Variant: Add apk to an apex
+android_app {
+    name: "CtsShimAddApkToApex",
+    sdk_version: "current",
+    srcs: ["shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java"],
+    optimize: {
+        enabled: false,
+    },
+    dex_preopt: {
+        enabled: false,
+    },
+    manifest: "shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.apex.cts.shim.v2_add_apk_to_apex",
+    ],
+}
diff --git a/packages/CtsShim/build/shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml b/packages/CtsShim/build/shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml
new file mode 100644
index 0000000..0e620b0
--- /dev/null
+++ b/packages/CtsShim/build/shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2023 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.addapktoapex.app">
+
+    <application>
+        <activity android:name=".AddApkToApexDeviceActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/packages/CtsShim/build/shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java b/packages/CtsShim/build/shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java
new file mode 100644
index 0000000..c68904b
--- /dev/null
+++ b/packages/CtsShim/build/shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.addapktoapex.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * A simple activity which logs to Logcat.
+ */
+public class AddApkToApexDeviceActivity extends Activity {
+
+    private static final String TAG = AddApkToApexDeviceActivity.class.getSimpleName();
+
+    /**
+     * The test string to log.
+     */
+    private static final String TEST_STRING = "AddApkToApexTestString";
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        // Log the test string to Logcat.
+        Log.i(TAG, TEST_STRING);
+    }
+
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index f2f019d9..736e0ef 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -34,7 +34,6 @@
 import android.os.Bundle;
 import android.os.Process;
 import android.os.UserManager;
-import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
@@ -51,12 +50,10 @@
 public class InstallStart extends Activity {
     private static final String TAG = InstallStart.class.getSimpleName();
 
-
-    private static final int DLG_INSTALL_APPS_RESTRICTED_FOR_USER = 1;
-    private static final int DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER = 2;
     private PackageManager mPackageManager;
     private UserManager mUserManager;
     private boolean mAbortInstall = false;
+    private boolean mShouldFinish = true;
 
     private final boolean mLocalLOGV = false;
 
@@ -131,7 +128,7 @@
             mAbortInstall = true;
         }
 
-        checkDevicePolicyRestriction();
+        checkDevicePolicyRestrictions();
 
         final String installerPackageNameFromIntent = getIntent().getStringExtra(
                 Intent.EXTRA_INSTALLER_PACKAGE_NAME);
@@ -150,7 +147,9 @@
 
         if (mAbortInstall) {
             setResult(RESULT_CANCELED);
-            finish();
+            if (mShouldFinish) {
+                finish();
+            }
             return;
         }
 
@@ -281,58 +280,52 @@
         return originatingUid == installerUid;
     }
 
-    private void checkDevicePolicyRestriction() {
-        // Check for install apps user restriction first.
-        final int installAppsRestrictionSource = mUserManager.getUserRestrictionSource(
-                UserManager.DISALLOW_INSTALL_APPS, Process.myUserHandle());
-        if ((installAppsRestrictionSource & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) {
-            if (mLocalLOGV) Log.i(TAG, "install not allowed: " + UserManager.DISALLOW_INSTALL_APPS);
-            mAbortInstall = true;
-            showDialogInner(DLG_INSTALL_APPS_RESTRICTED_FOR_USER);
-            return;
-        } else if (installAppsRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
-            if (mLocalLOGV) {
-                Log.i(TAG, "install not allowed by admin; showing "
-                        + Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS);
-            }
-            mAbortInstall = true;
-            startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS));
-            return;
-        }
+    private void checkDevicePolicyRestrictions() {
+        final String[] restrictions = new String[] {
+            UserManager.DISALLOW_INSTALL_APPS,
+            UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
+            UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
+        };
 
-        final int unknownSourcesRestrictionSource = mUserManager.getUserRestrictionSource(
-                UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, Process.myUserHandle());
-        final int unknownSourcesGlobalRestrictionSource = mUserManager.getUserRestrictionSource(
-                UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, Process.myUserHandle());
-        final int systemRestriction = UserManager.RESTRICTION_SOURCE_SYSTEM
-                & (unknownSourcesRestrictionSource | unknownSourcesGlobalRestrictionSource);
-        if (systemRestriction != 0) {
-            if (mLocalLOGV) Log.i(TAG, "Showing DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER");
+        final DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
+        for (String restriction : restrictions) {
+            if (!mUserManager.hasUserRestrictionForUser(restriction, Process.myUserHandle())) {
+                continue;
+            }
+
             mAbortInstall = true;
-            showDialogInner(DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER);
-        } else if (unknownSourcesRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
-            mAbortInstall = true;
-            startAdminSupportDetailsActivity(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
-        } else if (unknownSourcesGlobalRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
-            mAbortInstall = true;
-            startAdminSupportDetailsActivity(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY);
+
+            // If the given restriction is set by an admin, display information about the
+            // admin enforcing the restriction for the affected user. If not enforced by the admin,
+            // show the system dialog.
+            final Intent showAdminSupportDetailsIntent = dpm.createAdminSupportIntent(restriction);
+            if (showAdminSupportDetailsIntent != null) {
+                if (mLocalLOGV) Log.i(TAG, "starting " + showAdminSupportDetailsIntent);
+                startActivity(showAdminSupportDetailsIntent);
+            } else {
+                if (mLocalLOGV) Log.i(TAG, "Restriction set by system: " + restriction);
+                mShouldFinish = false;
+                showDialogInner(restriction);
+            }
+            break;
         }
     }
 
     /**
-     * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}.
+     * Replace any dialog shown by the dialog with the one for the given
+     * {@link #createDialog(String)}.
      *
-     * @param id The dialog type to add
+     * @param restriction The restriction to create the dialog for
      */
-    private void showDialogInner(int id) {
-        if (mLocalLOGV) Log.i(TAG, "showDialogInner(" + id + ")");
+    private void showDialogInner(String restriction) {
+        if (mLocalLOGV) Log.i(TAG, "showDialogInner(" + restriction + ")");
         DialogFragment currentDialog =
                 (DialogFragment) getFragmentManager().findFragmentByTag("dialog");
         if (currentDialog != null) {
             currentDialog.dismissAllowingStateLoss();
         }
 
-        DialogFragment newDialog = createDialog(id);
+        DialogFragment newDialog = createDialog(restriction);
         if (newDialog != null) {
             getFragmentManager().beginTransaction()
                     .add(newDialog, "dialog").commitAllowingStateLoss();
@@ -342,34 +335,20 @@
     /**
      * Create a new dialog.
      *
-     * @param id The id of the dialog (determines dialog type)
+     * @param restriction The restriction to create the dialog for
      * @return The dialog
      */
-    private DialogFragment createDialog(int id) {
-        if (mLocalLOGV) Log.i(TAG, "createDialog(" + id + ")");
-        switch (id) {
-            case DLG_INSTALL_APPS_RESTRICTED_FOR_USER:
+    private DialogFragment createDialog(String restriction) {
+        if (mLocalLOGV) Log.i(TAG, "createDialog(" + restriction + ")");
+        switch (restriction) {
+            case UserManager.DISALLOW_INSTALL_APPS:
                 return PackageUtil.SimpleErrorDialog.newInstance(
                         R.string.install_apps_user_restriction_dlg_text);
-            case DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER:
+            case UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES:
+            case UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY:
                 return PackageUtil.SimpleErrorDialog.newInstance(
                         R.string.unknown_apps_user_restriction_dlg_text);
         }
         return null;
     }
-
-    private void startAdminSupportDetailsActivity(String restriction) {
-        if (mLocalLOGV) Log.i(TAG, "startAdminSupportDetailsActivity(): " + restriction);
-
-        // If the given restriction is set by an admin, display information about the
-        // admin enforcing the restriction for the affected user.
-        final DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
-        final Intent showAdminSupportDetailsIntent = dpm.createAdminSupportIntent(restriction);
-        if (showAdminSupportDetailsIntent != null) {
-            if (mLocalLOGV) Log.i(TAG, "starting " + showAdminSupportDetailsIntent);
-            startActivity(showAdminSupportDetailsIntent);
-        } else {
-            if (mLocalLOGV) Log.w(TAG, "not intent for " + restriction);
-        }
-    }
 }
diff --git a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig
index 0b74fa8..d1bcb57 100644
--- a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig
@@ -2,7 +2,7 @@
 
 flag {
   name: "use_media_router2_for_info_media_manager"
-  namespace: "placeholder_namespace"
+  namespace: "media_solutions"
   description: "Gates whether to use a MediaRouter2-based implementation of InfoMediaManager, instead of the legacy MediaRouter2Manager-based implementation."
   bug: "192657812"
 }
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
index 9ab84d2..f90a17a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
@@ -45,6 +45,7 @@
     private static final int EXTREME_LOW_BATTERY_THRESHOLD = 3;
     private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000;
 
+    public static final int BATTERY_LEVEL_UNKNOWN = -1;
     public static final int CHARGING_UNKNOWN = -1;
     public static final int CHARGING_SLOWLY = 0;
     public static final int CHARGING_REGULAR = 1;
@@ -186,12 +187,13 @@
     /** Gets the battery level from the intent. */
     public static int getBatteryLevel(Intent batteryChangedIntent) {
         if (batteryChangedIntent == null) {
-            return -1; /*invalid battery level*/
+            return BATTERY_LEVEL_UNKNOWN;
         }
-        final int level = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+        final int level =
+                batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_UNKNOWN);
         final int scale = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
         return scale == 0
-                ? -1 /*invalid battery level*/
+                ? BATTERY_LEVEL_UNKNOWN
                 : Math.round((level / (float) scale) * 100f);
     }
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 423c8a3..2d62e2a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -242,6 +242,7 @@
         Settings.Secure.HEARING_AID_CALL_ROUTING,
         Settings.Secure.HEARING_AID_MEDIA_ROUTING,
         Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING,
-        Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED
+        Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
+        Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 6c9da97..4494765 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -200,6 +200,7 @@
         VALIDATORS.put(Secure.ASSIST_GESTURE_WAKE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"}));
         VALIDATORS.put(Secure.NOTIFICATION_BADGING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.NOTIFICATION_DISMISS_RTL, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1192e00..d2b444b 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1926,6 +1926,9 @@
         dumpSetting(s, p,
                 Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED,
                 SecureSettingsProto.Assist.LONG_PRESS_HOME_ENABLED);
+        dumpSetting(s, p,
+                Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED,
+                SecureSettingsProto.Assist.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED);
         p.end(assistToken);
 
         final long assistHandlesToken = p.start(SecureSettingsProto.ASSIST_HANDLES);
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 798bdec4..ab0225d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -409,6 +409,18 @@
             scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } }
         }
 
+    // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this
+    // is dependent on which clocks are connected, it may change when a clock is installed or
+    // removed from the device (unlike currentClockId).
+    // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors.
+    val activeClockId: String
+        get() {
+            if (!availableClocks.containsKey(currentClockId)) {
+                return DEFAULT_CLOCK_ID
+            }
+            return currentClockId
+        }
+
     init {
         // Register default clock designs
         for (clock in defaultClockProvider.getClocks()) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index e7a53e5b..b28920c 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -65,7 +65,7 @@
     protected var onSecondaryDisplay: Boolean = false
 
     override val events: DefaultClockEvents
-    override val config = ClockConfig()
+    override val config = ClockConfig(DEFAULT_CLOCK_ID)
 
     init {
         val parent = FrameLayout(ctx)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 527f8007..e2f4793 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -199,6 +199,8 @@
 
 /** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */
 data class ClockConfig(
+    val id: String,
+
     /** Transition to AOD should move smartspace like large clock instead of small clock */
     val useAlternateSmartspaceAODTransition: Boolean = false,
 
diff --git a/packages/SystemUI/res/drawable/auth_credential_emergency_button_background.xml b/packages/SystemUI/res/drawable/auth_credential_emergency_button_background.xml
new file mode 100644
index 0000000..85450b4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/auth_credential_emergency_button_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android">
+    <shape android:shape="rectangle">
+        <corners android:radius="25dp"/>
+        <solid android:color="@android:color/system_accent3_100" />
+    </shape>
+</inset>
diff --git a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
index 4a9d41f..b83f15a 100644
--- a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
+++ b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
@@ -14,6 +14,4 @@
     limitations under the License.
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:insetLeft="3dp"
-    android:insetRight="3dp"
     android:drawable="@drawable/ic_speaker_mute" />
diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
index e2ce34f..e439f77 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
@@ -61,29 +61,46 @@
 
     </RelativeLayout>
 
-    <LinearLayout
+    <FrameLayout
         android:id="@+id/auth_credential_input"
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
-        <ImeAwareEditText
-            android:id="@+id/lockPassword"
-            style="?passwordTextAppearance"
-            android:layout_width="208dp"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii"
-            android:inputType="textPassword"
-            android:minHeight="48dp" />
-
-        <TextView
-            android:id="@+id/error"
-            style="?errorTextAppearance"
-            android:layout_gravity="center"
+        <LinearLayout
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content" />
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal|top"
+            android:orientation="vertical">
 
-    </LinearLayout>
+            <ImeAwareEditText
+                android:id="@+id/lockPassword"
+                style="?passwordTextAppearance"
+                android:layout_width="208dp"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii"
+                android:inputType="textPassword"
+                android:minHeight="48dp"/>
+
+            <TextView
+                android:id="@+id/error"
+                style="?errorTextAppearance"
+                android:layout_gravity="center_horizontal"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"/>
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/emergencyCallButton"
+            style="@style/AuthCredentialEmergencyButtonStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="gone"
+            android:layout_gravity="center_horizontal|bottom"
+            android:layout_marginTop="12dp"
+            android:layout_marginBottom="12dp"
+            android:text="@string/work_challenge_emergency_button_text"/>
+    </FrameLayout>
 
 </com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
index 88f138f..d5af377 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
@@ -60,27 +60,44 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
 
+        <TextView
+            android:id="@+id/error"
+            style="?errorTextAppearanceLand"
+            android:layout_below="@id/description"
+            android:layout_alignParentLeft="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
     </RelativeLayout>
 
-    <FrameLayout
+    <RelativeLayout
         android:layout_weight="1"
-        style="?containerStyle"
         android:layout_width="0dp"
         android:layout_height="match_parent">
 
-        <com.android.internal.widget.LockPatternView
-            android:id="@+id/lockPattern"
-            android:layout_gravity="center"
-            android:layout_width="@dimen/biometric_auth_pattern_view_size"
-            android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
-
-        <TextView
-            android:id="@+id/error"
-            style="?errorTextAppearance"
+        <FrameLayout
+            style="?containerStyle"
+            android:layout_above="@id/emergencyCallButton"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal|bottom"/>
+            android:layout_height="match_parent">
 
-    </FrameLayout>
+            <com.android.internal.widget.LockPatternView
+                android:id="@+id/lockPattern"
+                android:layout_gravity="center"
+                android:layout_width="@dimen/biometric_auth_pattern_view_size"
+                android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
+        </FrameLayout>
+
+        <Button
+            android:id="@+id/emergencyCallButton"
+            style="@style/AuthCredentialEmergencyButtonStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="35dp"
+            android:visibility="gone"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:text="@string/work_challenge_emergency_button_text"/>
+    </RelativeLayout>
 
 </com.android.systemui.biometrics.ui.CredentialPatternView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml
index 33f1b10..9336845 100644
--- a/packages/SystemUI/res/layout/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml
@@ -65,29 +65,46 @@
 
     </ScrollView>
 
-    <LinearLayout
+    <FrameLayout
         android:id="@+id/auth_credential_input"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
-        <ImeAwareEditText
-            android:id="@+id/lockPassword"
-            style="?passwordTextAppearance"
-            android:layout_width="208dp"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal"
-            android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii"
-            android:inputType="textPassword"
-            android:minHeight="48dp" />
-
-        <TextView
-            android:id="@+id/error"
-            style="?errorTextAppearance"
-            android:layout_gravity="center_horizontal"
+        <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal|top"
+            android:orientation="vertical">
 
-    </LinearLayout>
+            <ImeAwareEditText
+                android:id="@+id/lockPassword"
+                style="?passwordTextAppearance"
+                android:layout_width="208dp"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii"
+                android:inputType="textPassword"
+                android:minHeight="48dp"/>
+
+            <TextView
+                android:id="@+id/error"
+                style="?errorTextAppearance"
+                android:layout_gravity="center_horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"/>
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/emergencyCallButton"
+            style="@style/AuthCredentialEmergencyButtonStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="gone"
+            android:layout_gravity="center_horizontal|bottom"
+            android:layout_marginTop="12dp"
+            android:layout_marginBottom="12dp"
+            android:text="@string/work_challenge_emergency_button_text"/>
+    </FrameLayout>
 
 </com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
index 81ca3718..59828fd 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -58,24 +58,42 @@
             android:layout_height="wrap_content"/>
     </RelativeLayout>
 
-    <FrameLayout
+    <RelativeLayout
         android:id="@+id/auth_credential_container"
-        style="?containerStyle"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
-        <com.android.internal.widget.LockPatternView
-            android:id="@+id/lockPattern"
-            android:layout_gravity="center"
-            android:layout_width="@dimen/biometric_auth_pattern_view_size"
-            android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
+        <FrameLayout
+            android:layout_centerInParent="true"
+            android:layout_above="@id/emergencyCallButton"
+            style="?containerStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent">
 
-        <TextView
-            android:id="@+id/error"
-            style="?errorTextAppearance"
-            android:layout_width="match_parent"
+            <com.android.internal.widget.LockPatternView
+                android:id="@+id/lockPattern"
+                android:layout_gravity="center"
+                android:layout_width="@dimen/biometric_auth_pattern_view_size"
+                android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
+
+            <TextView
+                android:id="@+id/error"
+                style="?errorTextAppearance"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal|bottom"/>
+        </FrameLayout>
+
+        <Button
+            android:id="@+id/emergencyCallButton"
+            style="@style/AuthCredentialEmergencyButtonStyle"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal|bottom"/>
-    </FrameLayout>
+            android:layout_alignParentBottom="true"
+            android:visibility="gone"
+            android:layout_marginBottom="35dp"
+            android:layout_centerHorizontal="true"
+            android:text="@string/work_challenge_emergency_button_text"/>
+    </RelativeLayout>
 
 </com.android.systemui.biometrics.ui.CredentialPatternView>
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
index 78cd718..39ec09b 100644
--- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -34,8 +34,8 @@
         android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
         android:contentDescription="@string/screenshot_dismiss_work_profile">
         <ImageView
-            android:layout_width="16dp"
-            android:layout_height="16dp"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
             android:layout_gravity="center"
             android:background="@drawable/circular_background"
             android:backgroundTint="?androidprv:attr/materialColorSurfaceContainerHigh"
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index d693631..8bc3eed 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -221,6 +221,7 @@
         <attr name="descriptionTextAppearance" format="reference" />
         <attr name="passwordTextAppearance" format="reference" />
         <attr name="errorTextAppearance" format="reference"/>
+        <attr name="errorTextAppearanceLand" format="reference"/>
     </declare-styleable>
 
     <declare-styleable name="LogAccessPermissionGrantDialog">
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index cddfda2..2003fa3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -386,6 +386,8 @@
     <string name="biometric_dialog_wrong_password">Wrong password</string>
     <!-- Error string shown when the user enters too many incorrect attempts [CHAR LIMIT=120]-->
     <string name="biometric_dialog_credential_too_many_attempts">Too many incorrect attempts.\nTry again in <xliff:g id="number">%d</xliff:g> seconds.</string>
+    <!-- Button text shown on an authentication screen giving the user the option to make an emergency call without unlocking their device [CHAR LIMIT=20] -->
+    <string name="work_challenge_emergency_button_text">Emergency</string>
 
     <!-- Error string shown when the user enters an incorrect PIN/pattern/password and it counts towards the max attempts before the data on the device is wiped. [CHAR LIMIT=NONE]-->
     <string name="biometric_dialog_credential_attempts_before_wipe">Try again. Attempt <xliff:g id="attempts" example="1">%1$d</xliff:g> of <xliff:g id="max_attempts" example="3">%2$d</xliff:g>.</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 10340c6..6991b96 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -236,6 +236,13 @@
         <item name="android:gravity">center</item>
     </style>
 
+    <style name="TextAppearance.AuthNonBioCredential.ErrorLand">
+        <item name="android:layout_marginTop">20dp</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">?android:attr/colorError</item>
+        <item name="android:gravity">start</item>
+    </style>
+
     <style name="TextAppearance.AuthCredential.PasswordEntry" parent="@android:style/TextAppearance.DeviceDefault">
         <item name="android:gravity">center</item>
         <item name="android:paddingTop">28dp</item>
@@ -276,6 +283,17 @@
         <item name="android:minWidth">200dp</item>
     </style>
 
+    <style name="AuthCredentialEmergencyButtonStyle">
+        <item name="android:background">@drawable/auth_credential_emergency_button_background</item>
+        <item name="android:textColor">@android:color/system_accent3_900</item>
+        <item name="android:outlineProvider">none</item>
+        <item name="android:paddingTop">15dp</item>
+        <item name="android:paddingBottom">15dp</item>
+        <item name="android:paddingLeft">30dp</item>
+        <item name="android:paddingRight">30dp</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
         <item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
@@ -353,6 +371,7 @@
         <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item>
         <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item>
         <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item>
+        <item name="errorTextAppearanceLand">@style/TextAppearance.AuthNonBioCredential.ErrorLand</item>
     </style>
 
     <style name="LockPatternViewStyle" >
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index b589887..06b6692 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -39,6 +39,7 @@
 public class KeyguardClockSwitch extends RelativeLayout {
 
     private static final String TAG = "KeyguardClockSwitch";
+    public static final String MISSING_CLOCK_ID = "CLOCK_MISSING";
 
     private static final long CLOCK_OUT_MILLIS = 133;
     private static final long CLOCK_IN_MILLIS = 167;
@@ -196,6 +197,14 @@
         return mLogBuffer;
     }
 
+    /** Returns the id of the currently rendering clock */
+    public String getClockId() {
+        if (mClock == null) {
+            return MISSING_CLOCK_ID;
+        }
+        return mClock.getConfig().getId();
+    }
+
     void setClock(ClockController clock, int statusBarState) {
         mClock = clock;
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index dd39f1d..05ace74 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -222,8 +222,10 @@
         mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
         mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
 
-        mDumpManager.unregisterDumpable(getClass().getSimpleName()); // unregister previous clocks
-        mDumpManager.registerDumpable(getClass().getSimpleName(), this);
+        if (!mOnlyClock) {
+            mDumpManager.unregisterDumpable(getClass().getSimpleName()); // unregister previous
+            mDumpManager.registerDumpable(getClass().getSimpleName(), this);
+        }
 
         if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) {
             mStatusArea = mView.findViewById(R.id.keyguard_status_area);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 04692c4..9f3908a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -27,6 +27,7 @@
 import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER;
 import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE;
 import static com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES;
 
 import android.app.ActivityManager;
@@ -370,8 +371,12 @@
 
                 @Override
                 public void onOrientationChanged(int orientation) {
-                    KeyguardSecurityContainerController.this
+                    if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)) {
+                        // TODO(b/295603468)
+                        // Fix reinflation of views when flag is enabled.
+                        KeyguardSecurityContainerController.this
                             .onDensityOrFontScaleOrOrientationChanged();
+                    }
                 }
             };
     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index caebc30..a3ee220 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -16,6 +16,7 @@
     val description: String,
     val userInfo: BiometricUserInfo,
     val operationInfo: BiometricOperationInfo,
+    val showEmergencyCallButton: Boolean,
 ) {
     /** Prompt using one or more biometrics. */
     class Biometric(
@@ -29,7 +30,8 @@
             subtitle = info.subtitle?.toString() ?: "",
             description = info.description?.toString() ?: "",
             userInfo = userInfo,
-            operationInfo = operationInfo
+            operationInfo = operationInfo,
+            showEmergencyCallButton = info.isShowEmergencyCallButton
         ) {
         val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
     }
@@ -46,6 +48,7 @@
             description = (info.deviceCredentialDescription ?: info.description)?.toString() ?: "",
             userInfo = userInfo,
             operationInfo = operationInfo,
+            showEmergencyCallButton = info.isShowEmergencyCallButton
         ) {
 
         /** PIN prompt. */
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
index 9292bd7..4ac9f96 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -2,6 +2,7 @@
 
 import android.view.View
 import android.view.ViewGroup
+import android.widget.Button
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.lifecycle.Lifecycle
@@ -46,6 +47,7 @@
         val descriptionView: TextView = view.requireViewById(R.id.description)
         val iconView: ImageView? = view.findViewById(R.id.icon)
         val errorView: TextView = view.requireViewById(R.id.error)
+        val emergencyButtonView: Button = view.requireViewById(R.id.emergencyCallButton)
 
         var errorTimer: Job? = null
 
@@ -75,6 +77,13 @@
 
                         iconView?.setImageDrawable(header.icon)
 
+                        if (header.showEmergencyCallButton) {
+                            emergencyButtonView.visibility = View.VISIBLE
+                            emergencyButtonView.setOnClickListener {
+                                viewModel.doEmergencyCall(view.context)
+                            }
+                        }
+
                         // Only animate this if we're transitioning from a biometric view.
                         if (viewModel.animateContents.value) {
                             view.animateCredentialViewIn()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
index 3257f20..c6d9085 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
@@ -10,4 +10,5 @@
     val subtitle: String
     val description: String
     val icon: Drawable
+    val showEmergencyCallButton: Boolean
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
index a3b23ca..6212ef0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -41,6 +41,7 @@
                 subtitle = request.subtitle,
                 description = request.description,
                 icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId),
+                showEmergencyCallButton = request.showEmergencyCallButton
             )
         }
 
@@ -136,6 +137,18 @@
             }
         }
     }
+
+    fun doEmergencyCall(context: Context) {
+        val intent =
+            context
+                .getSystemService(android.telecom.TelecomManager::class.java)!!
+                .createLaunchEmergencyDialerIntent(null)
+                .setFlags(
+                    android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
+                            android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
+                )
+        context.startActivity(intent)
+    }
 }
 
 private fun Context.asBadCredentialErrorMessage(prompt: BiometricPromptRequest?): String =
@@ -174,6 +187,7 @@
     override val subtitle: String,
     override val description: String,
     override val icon: Drawable,
+    override val showEmergencyCallButton: Boolean,
 ) : CredentialHeaderViewModel
 
 private fun CredentialHeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 970d00b..3bf9cc8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -107,6 +107,7 @@
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.LetterboxModule;
+import com.android.systemui.statusbar.phone.NotificationIconAreaControllerModule;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -185,6 +186,7 @@
             MediaProjectionModule.class,
             MediaProjectionTaskSwitcherModule.class,
             MotionToolModule.class,
+            NotificationIconAreaControllerModule.class,
             PeopleHubModule.class,
             PeopleModule.class,
             PluginModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index f3900ac..1516f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -355,7 +355,7 @@
 
     // TODO(b/278068252): Tracking Bug
     @JvmField
-    val QS_PIPELINE_AUTO_ADD = unreleasedFlag("qs_pipeline_auto_add", teamfood = false)
+    val QS_PIPELINE_AUTO_ADD = unreleasedFlag("qs_pipeline_auto_add", teamfood = true)
 
     // TODO(b/254512383): Tracking Bug
     @JvmField
@@ -512,11 +512,6 @@
     val ENABLE_FLING_TO_DISMISS_PIP =
         sysPropBooleanFlag("persist.wm.debug.fling_to_dismiss_pip", default = true)
 
-    @Keep
-    @JvmField
-    val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
-        sysPropBooleanFlag("persist.wm.debug.enable_pip_keep_clear_algorithm", default = true)
-
     // TODO(b/256873975): Tracking Bug
     @JvmField
     @Keep
@@ -628,6 +623,10 @@
     // TODO(b/251205791): Tracking Bug
     @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips")
 
+    /** TODO(b/295143676): Tracking bug. When enable, captures a screenshot for each display. */
+    @JvmField
+    val MULTI_DISPLAY_SCREENSHOT = unreleasedFlag("multi_display_screenshot")
+
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
     val QUICK_TAP_IN_PCC = releasedFlag("quick_tap_in_pcc")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 562c4db..ed84884 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -40,10 +40,10 @@
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -53,7 +53,6 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
-import javax.inject.Inject
 
 /**
  * Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -68,18 +67,6 @@
     bouncerRepository: KeyguardBouncerRepository,
     configurationRepository: ConfigurationRepository,
 ) {
-
-    data class PreviewMode(
-        val isInPreviewMode: Boolean = false,
-        val shouldHighlightSelectedAffordance: Boolean = false,
-    )
-
-    /**
-     * Whether this view-model instance is powering the preview experience that renders exclusively
-     * in the wallpaper picker application. This should _always_ be `false` for the real lock screen
-     * experience.
-     */
-    val previewMode = MutableStateFlow(PreviewMode())
     /**
      * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
      * all.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index 635961b..e501ece 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -54,7 +54,11 @@
         if (event.handleAction()) {
             when (event.keyCode) {
                 KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent()
-                KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent()
+                KeyEvent.KEYCODE_SPACE,
+                KeyEvent.KEYCODE_ENTER ->
+                    if (isDeviceInteractive()) {
+                        return collapseShadeLockedOrShowPrimaryBouncer()
+                    }
             }
         }
         return false
@@ -90,16 +94,22 @@
                 (statusBarStateController.state != StatusBarState.SHADE) &&
                 statusBarKeyguardViewManager.shouldDismissOnMenuPressed()
         if (shouldUnlockOnMenuPressed) {
-            shadeController.animateCollapseShadeForced()
-            return true
+            return collapseShadeLockedOrShowPrimaryBouncer()
         }
         return false
     }
 
-    private fun dispatchSpaceEvent(): Boolean {
-        if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) {
-            shadeController.animateCollapseShadeForced()
-            return true
+    private fun collapseShadeLockedOrShowPrimaryBouncer(): Boolean {
+        when (statusBarStateController.state) {
+            StatusBarState.SHADE -> return false
+            StatusBarState.SHADE_LOCKED -> {
+                shadeController.animateCollapseShadeForced()
+                return true
+            }
+            StatusBarState.KEYGUARD -> {
+                statusBarKeyguardViewManager.showPrimaryBouncer(true)
+                return true
+            }
         }
         return false
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index dd3da97..41c1c96 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -49,6 +49,7 @@
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
+import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
@@ -56,25 +57,31 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
 import com.android.systemui.monet.ColorScheme
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.shared.clocks.ClockRegistry
 import com.android.systemui.shared.clocks.DefaultClockController
 import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.runBlocking
 
 /** Renders the preview of the lock screen. */
 class KeyguardPreviewRenderer
+@OptIn(ExperimentalCoroutinesApi::class)
 @AssistedInject
 constructor(
     @Application private val context: Context,
@@ -99,6 +106,9 @@
     @Assisted bundle: Bundle,
     private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
     private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+    private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
+    private val chipbarCoordinator: ChipbarCoordinator,
+    private val keyguardStateController: KeyguardStateController,
 ) {
 
     val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
@@ -130,19 +140,21 @@
 
     init {
         if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
-            keyguardRootViewModel.enablePreviewMode(
+            keyguardRootViewModel.enablePreviewMode()
+            quickAffordancesCombinedViewModel.enablePreviewMode(
                 initiallySelectedSlotId =
-                bundle.getString(
-                    KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
-                ),
+                    bundle.getString(
+                        KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
+                    )
+                        ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                 shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
             )
         } else {
             bottomAreaViewModel.enablePreviewMode(
                 initiallySelectedSlotId =
-                bundle.getString(
-                    KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
-                ),
+                    bundle.getString(
+                        KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
+                    ),
                 shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
             )
         }
@@ -163,17 +175,8 @@
             val rootView = FrameLayout(context)
 
             if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
-                val keyguardRootView = KeyguardRootView(context, null)
-                rootView.addView(
-                    keyguardRootView,
-                    FrameLayout.LayoutParams(
-                        FrameLayout.LayoutParams.MATCH_PARENT,
-                        FrameLayout.LayoutParams.MATCH_PARENT,
-                    ),
-                )
-                setupShortcuts(keyguardRootView)
-                KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
-                keyguardBlueprintInteractor.refreshBlueprint()
+                setupKeyguardRootView(rootView)
+                setupShortcuts(rootView)
             } else {
                 setUpBottomArea(rootView)
             }
@@ -314,7 +317,7 @@
                     false,
                 ) as KeyguardBottomAreaView
         bottomAreaView.init(
-                viewModel = bottomAreaViewModel,
+            viewModel = bottomAreaViewModel,
         )
         parentView.addView(
             bottomAreaView,
@@ -325,10 +328,34 @@
         )
     }
 
-    private fun setupShortcuts(keyguardRootView: KeyguardRootView) {
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun setupKeyguardRootView(rootView: FrameLayout) {
+        val keyguardRootView = KeyguardRootView(context, null)
+        disposables.add(
+            KeyguardRootViewBinder.bind(
+                keyguardRootView,
+                keyguardRootViewModel,
+                featureFlags,
+                occludingAppDeviceEntryMessageViewModel,
+                chipbarCoordinator,
+                keyguardStateController,
+            )
+        )
+        rootView.addView(
+            keyguardRootView,
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT,
+            ),
+        )
+        KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
+        keyguardBlueprintInteractor.refreshBlueprint()
+    }
+
+    private fun setupShortcuts(rootView: FrameLayout) {
         shortcutsBindings.add(
             KeyguardQuickAffordanceViewBinder.bind(
-                keyguardRootView.requireViewById(R.id.start_button),
+                rootView.requireViewById(R.id.start_button),
                 quickAffordancesCombinedViewModel.startButton,
                 keyguardRootViewModel.alpha,
                 falsingManager,
@@ -340,7 +367,7 @@
 
         shortcutsBindings.add(
             KeyguardQuickAffordanceViewBinder.bind(
-                keyguardRootView.requireViewById(R.id.end_button),
+                rootView.requireViewById(R.id.end_button),
                 quickAffordancesCombinedViewModel.endButton,
                 keyguardRootViewModel.alpha,
                 falsingManager,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
index 56a98455..02ea550 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
@@ -18,22 +18,20 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import androidx.annotation.VisibleForTesting
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import javax.inject.Inject
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class KeyguardQuickAffordancesCombinedViewModel
@@ -43,6 +41,18 @@
     private val keyguardInteractor: KeyguardInteractor,
 ) {
 
+    data class PreviewMode(
+        val isInPreviewMode: Boolean = false,
+        val shouldHighlightSelectedAffordance: Boolean = false,
+    )
+
+    /**
+     * Whether this view-model instance is powering the preview experience that renders exclusively
+     * in the wallpaper picker application. This should _always_ be `false` for the real lock screen
+     * experience.
+     */
+    private val previewMode = MutableStateFlow(PreviewMode())
+
     /**
      * ID of the slot that's currently selected in the preview that renders exclusively in the
      * wallpaper picker application. This is ignored for the actual, real lock screen experience.
@@ -85,39 +95,63 @@
         selectedPreviewSlotId.value = slotId
     }
 
+    /**
+     * Puts this view-model in "preview mode", which means it's being used for UI that is rendering
+     * the lock screen preview in wallpaper picker / settings and not the real experience on the
+     * lock screen.
+     *
+     * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
+     * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be
+     *   highlighted (while all others are dimmed to make the selected one stand out).
+     */
+    fun enablePreviewMode(
+        initiallySelectedSlotId: String?,
+        shouldHighlightSelectedAffordance: Boolean,
+    ) {
+        val newPreviewMode =
+            PreviewMode(
+                isInPreviewMode = true,
+                shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
+            )
+        onPreviewSlotSelected(
+            initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
+        )
+        previewMode.value = newPreviewMode
+    }
+
     private fun button(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceViewModel> {
-        return keyguardInteractor.previewMode.flatMapLatest { previewMode ->
+        return previewMode.flatMapLatest { previewMode ->
             combine(
-                if (previewMode.isInPreviewMode) {
-                    quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
-                } else {
-                    quickAffordanceInteractor.quickAffordance(position = position)
-                },
-                keyguardInteractor.animateDozingTransitions.distinctUntilChanged(),
-                areQuickAffordancesFullyOpaque,
-                selectedPreviewSlotId,
-                quickAffordanceInteractor.useLongPress(),
-            ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
-                val slotId = position.toSlotId()
-                val isSelected = selectedPreviewSlotId == slotId
-                model.toViewModel(
-                    animateReveal = !previewMode.isInPreviewMode && animateReveal,
-                    isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
-                    isSelected =
-                    previewMode.isInPreviewMode &&
-                        previewMode.shouldHighlightSelectedAffordance &&
-                        isSelected,
-                    isDimmed =
-                    previewMode.isInPreviewMode &&
-                        previewMode.shouldHighlightSelectedAffordance &&
-                        !isSelected,
-                    forceInactive = previewMode.isInPreviewMode,
-                    slotId = slotId,
-                    useLongPress = useLongPress,
-                )
-            }
+                    if (previewMode.isInPreviewMode) {
+                        quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
+                    } else {
+                        quickAffordanceInteractor.quickAffordance(position = position)
+                    },
+                    keyguardInteractor.animateDozingTransitions.distinctUntilChanged(),
+                    areQuickAffordancesFullyOpaque,
+                    selectedPreviewSlotId,
+                    quickAffordanceInteractor.useLongPress(),
+                ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
+                    val slotId = position.toSlotId()
+                    val isSelected = selectedPreviewSlotId == slotId
+                    model.toViewModel(
+                        animateReveal = !previewMode.isInPreviewMode && animateReveal,
+                        isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
+                        isSelected =
+                            previewMode.isInPreviewMode &&
+                                previewMode.shouldHighlightSelectedAffordance &&
+                                isSelected,
+                        isDimmed =
+                            previewMode.isInPreviewMode &&
+                                previewMode.shouldHighlightSelectedAffordance &&
+                                !isSelected,
+                        forceInactive = previewMode.isInPreviewMode,
+                        slotId = slotId,
+                        useLongPress = useLongPress,
+                    )
+                }
                 .distinctUntilChanged()
         }
     }
@@ -167,8 +201,6 @@
         // time, we don't want the number to be too close to 1.0 such that there is a chance that we
         // never treat the affordance UI as "fully opaque" as that would risk making it forever not
         // clickable.
-        @VisibleForTesting
-        const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f
+        @VisibleForTesting const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f
     }
-
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 316ca77..92b9ee4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -19,29 +19,37 @@
 
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState
-import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
-import javax.inject.Inject
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class KeyguardRootViewModel
 @Inject
 constructor(
     private val keyguardInteractor: KeyguardInteractor,
-    private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
-)
-{
+) {
+
+    data class PreviewMode(val isInPreviewMode: Boolean = false)
+
+    /**
+     * Whether this view-model instance is powering the preview experience that renders exclusively
+     * in the wallpaper picker application. This should _always_ be `false` for the real lock screen
+     * experience.
+     */
+    private val previewMode = MutableStateFlow(PreviewMode())
+
     /** Represents the current state of the KeyguardRootView visibility */
     val keyguardRootViewVisibilityState: Flow<KeyguardRootViewVisibilityState> =
         keyguardInteractor.keyguardRootViewVisibilityState
 
     /** An observable for the alpha level for the entire keyguard root view. */
     val alpha: Flow<Float> =
-        keyguardInteractor.previewMode.flatMapLatest {
+        previewMode.flatMapLatest {
             if (it.isInPreviewMode) {
                 flowOf(1f)
             } else {
@@ -53,23 +61,9 @@
      * Puts this view-model in "preview mode", which means it's being used for UI that is rendering
      * the lock screen preview in wallpaper picker / settings and not the real experience on the
      * lock screen.
-     *
-     * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
-     * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be
-     *   highlighted (while all others are dimmed to make the selected one stand out).
      */
-    fun enablePreviewMode(
-        initiallySelectedSlotId: String?,
-        shouldHighlightSelectedAffordance: Boolean,
-    ) {
-        keyguardInteractor.previewMode.value =
-            KeyguardInteractor.PreviewMode(
-                isInPreviewMode = true,
-                shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
-            )
-        keyguardQuickAffordancesCombinedViewModel.onPreviewSlotSelected(
-            initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
-        )
+    fun enablePreviewMode() {
+        val newPreviewMode = PreviewMode(true)
+        previewMode.value = newPreviewMode
     }
-
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
index f8f784f..3d4fca1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
@@ -216,6 +216,58 @@
         }
     }
 
+    private void stopSound(Command cmd) {
+        final MediaPlayer mp;
+        synchronized (mPlayerLock) {
+            mp = mPlayer;
+            mPlayer = null;
+        }
+        if (mp == null) {
+            Log.w(mTag, "STOP command without a player");
+            return;
+        }
+
+        long delay = SystemClock.uptimeMillis() - cmd.requestTime;
+        if (delay > 1000) {
+            Log.w(mTag, "Notification stop delayed by " + delay + "msecs");
+        }
+        try {
+            mp.stop();
+        } catch (Exception e) {
+            Log.w(mTag, "Failed to stop MediaPlayer", e);
+        }
+        if (DEBUG) {
+            Log.i(mTag, "About to release MediaPlayer piid:"
+                    + mp.getPlayerIId() + " due to notif cancelled");
+        }
+        try {
+            mp.release();
+        } catch (Exception e) {
+            Log.w(mTag, "Failed to release MediaPlayer", e);
+        }
+        synchronized (mQueueAudioFocusLock) {
+            if (mAudioManagerWithAudioFocus != null) {
+                if (DEBUG) {
+                    Log.d(mTag, "in STOP: abandonning AudioFocus");
+                }
+                try {
+                    mAudioManagerWithAudioFocus.abandonAudioFocus(null);
+                } catch (Exception e) {
+                    Log.w(mTag, "Failed to abandon audio focus", e);
+                }
+                mAudioManagerWithAudioFocus = null;
+            }
+        }
+        synchronized (mCompletionHandlingLock) {
+            if ((mLooper != null) && (mLooper.getThread().getState() != Thread.State.TERMINATED)) {
+                if (DEBUG) {
+                    Log.d(mTag, "in STOP: quitting looper " + mLooper);
+                }
+                mLooper.quit();
+            }
+        }
+    }
+
     private final class CmdThread extends java.lang.Thread {
         CmdThread() {
             super("NotificationPlayer-" + mTag);
@@ -229,62 +281,28 @@
                     if (DEBUG) Log.d(mTag, "RemoveFirst");
                     cmd = mCmdQueue.removeFirst();
                 }
-
-                switch (cmd.code) {
-                case PLAY:
-                    if (DEBUG) Log.d(mTag, "PLAY");
-                    startSound(cmd);
-                    break;
-                case STOP:
-                    if (DEBUG) Log.d(mTag, "STOP");
-                    final MediaPlayer mp;
-                    synchronized (mPlayerLock) {
-                        mp = mPlayer;
-                        mPlayer = null;
+                try {
+                    switch (cmd.code) {
+                        case PLAY:
+                            if (DEBUG) Log.d(mTag, "PLAY");
+                            startSound(cmd);
+                            break;
+                        case STOP:
+                            if (DEBUG) Log.d(mTag, "STOP");
+                            stopSound(cmd);
+                            break;
                     }
-                    if (mp != null) {
-                        long delay = SystemClock.uptimeMillis() - cmd.requestTime;
-                        if (delay > 1000) {
-                            Log.w(mTag, "Notification stop delayed by " + delay + "msecs");
+                } finally {
+                    synchronized (mCmdQueue) {
+                        if (mCmdQueue.size() == 0) {
+                            // nothing left to do, quit
+                            // doing this check after we're done prevents the case where they
+                            // added it during the operation from spawning two threads and
+                            // trying to do them in parallel.
+                            mThread = null;
+                            releaseWakeLock();
+                            return;
                         }
-                        try {
-                            mp.stop();
-                        } catch (Exception e) { }
-                        if (DEBUG) {
-                            Log.i(mTag, "About to release MediaPlayer piid:"
-                                    + mp.getPlayerIId() + " due to notif cancelled");
-                        }
-                        mp.release();
-                        synchronized(mQueueAudioFocusLock) {
-                            if (mAudioManagerWithAudioFocus != null) {
-                                if (DEBUG) { Log.d(mTag, "in STOP: abandonning AudioFocus"); }
-                                mAudioManagerWithAudioFocus.abandonAudioFocus(null);
-                                mAudioManagerWithAudioFocus = null;
-                            }
-                        }
-                        synchronized (mCompletionHandlingLock) {
-                            if ((mLooper != null) &&
-                                    (mLooper.getThread().getState() != Thread.State.TERMINATED))
-                            {
-                                if (DEBUG) { Log.d(mTag, "in STOP: quitting looper "+ mLooper); }
-                                mLooper.quit();
-                            }
-                        }
-                    } else {
-                        Log.w(mTag, "STOP command without a player");
-                    }
-                    break;
-                }
-
-                synchronized (mCmdQueue) {
-                    if (mCmdQueue.size() == 0) {
-                        // nothing left to do, quit
-                        // doing this check after we're done prevents the case where they
-                        // added it during the operation from spawning two threads and
-                        // trying to do them in parallel.
-                        mThread = null;
-                        releaseWakeLock();
-                        return;
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index a3d1d8c..d8824983 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -515,11 +515,13 @@
             mSeekBar.setOnTouchListener((v, event) -> false);
             updateIconAreaClickListener((v) -> {
                 if (device.getCurrentVolume() == 0) {
+                    mController.logInteractionUnmuteDevice(device);
                     mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME);
                     mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME);
                     updateUnmutedVolumeIcon();
                     mIconAreaLayout.setOnTouchListener(((iconV, event) -> false));
                 } else {
+                    mController.logInteractionMuteDevice(device);
                     mSeekBar.resetVolume();
                     mController.adjustVolume(device, 0);
                     updateMutedVolumeIcon();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index d281f50..bb0e9d1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -837,6 +837,14 @@
         mMetricLogger.logInteractionAdjustVolume(device);
     }
 
+    void logInteractionMuteDevice(MediaDevice device) {
+        mMetricLogger.logInteractionMute(device);
+    }
+
+    void logInteractionUnmuteDevice(MediaDevice device) {
+        mMetricLogger.logInteractionUnmute(device);
+    }
+
     String getPackageName() {
         return mPackageName;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
index 412d1a3..ffd626a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
@@ -154,6 +154,38 @@
     }
 
     /**
+     * Do the metric logging of muting device.
+     */
+    public void logInteractionMute(MediaDevice source) {
+        if (DEBUG) {
+            Log.d(TAG, "logInteraction - Mute");
+        }
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__MUTE,
+                getInteractionDeviceType(source),
+                getLoggingPackageName(),
+                source.isSuggestedDevice());
+    }
+
+    /**
+     * Do the metric logging of unmuting device.
+     */
+    public void logInteractionUnmute(MediaDevice source) {
+        if (DEBUG) {
+            Log.d(TAG, "logInteraction - Unmute");
+        }
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__UNMUTE,
+                getInteractionDeviceType(source),
+                getLoggingPackageName(),
+                source.isSuggestedDevice());
+    }
+
+    /**
      * Do the metric logging of content switching failure.
      *
      * @param deviceItemList media item list for device count updating
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt
index 67927a4..cb87e3c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt
@@ -22,13 +22,12 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.qs.QSTileView
 import com.android.systemui.qs.external.TileServiceRequestController
 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -37,10 +36,11 @@
 
 /**
  * Adapter to determine what real class to use for classes that depend on [QSHost].
- * * When [Flags.QS_PIPELINE_NEW_HOST] is off, all calls will be routed to [QSTileHost].
- * * When [Flags.QS_PIPELINE_NEW_HOST] is on, calls regarding the current set of tiles will be
- *   routed to [CurrentTilesInteractor]. Other calls (like [createTileView]) will still be routed to
+ * * When [QSPipelineFlagsRepository.pipelineHostEnabled] is false, all calls will be routed to
  *   [QSTileHost].
+ * * When [QSPipelineFlagsRepository.pipelineHostEnabled] is true, calls regarding the current set
+ *   of tiles will be routed to [CurrentTilesInteractor]. Other calls (like [createTileView]) will
+ *   still be routed to [QSTileHost].
  *
  * This routing also includes dumps.
  */
@@ -53,7 +53,7 @@
     private val context: Context,
     private val tileServiceRequestControllerBuilder: TileServiceRequestController.Builder,
     @Application private val scope: CoroutineScope,
-    private val featureFlags: FeatureFlags,
+    flags: QSPipelineFlagsRepository,
     dumpManager: DumpManager,
 ) : QSHost {
 
@@ -61,7 +61,7 @@
         private const val TAG = "QSTileHost"
     }
 
-    private val useNewHost = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)
+    private val useNewHost = flags.pipelineHostEnabled
 
     @GuardedBy("callbacksMap") private val callbacksMap = mutableMapOf<QSHost.Callback, Job>()
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 432147f..e57db56 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -35,8 +35,6 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.nano.SystemUIProtoDump;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.plugins.qs.QSFactory;
@@ -50,6 +48,7 @@
 import com.android.systemui.qs.nano.QsTileState;
 import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository;
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository;
 import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
@@ -119,7 +118,7 @@
 
     private TileLifecycleManager.Factory mTileLifeCycleManagerFactory;
 
-    private final FeatureFlags mFeatureFlags;
+    private final QSPipelineFlagsRepository mFeatureFlags;
 
     @Inject
     public QSTileHost(Context context,
@@ -135,7 +134,7 @@
             CustomTileStatePersister customTileStatePersister,
             TileLifecycleManager.Factory tileLifecycleManagerFactory,
             UserFileManager userFileManager,
-            FeatureFlags featureFlags
+            QSPipelineFlagsRepository featureFlags
     ) {
         mContext = context;
         mUserContext = context;
@@ -162,7 +161,7 @@
             // finishes before creating any tiles.
             tunerService.addTunable(this, TILES_SETTING);
             // AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
-            if (!mFeatureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) {
+            if (!mFeatureFlags.getPipelineAutoAddEnabled()) {
                 mAutoTiles = autoTiles.get();
             }
         });
@@ -283,7 +282,7 @@
             }
         }
         // Do not process tiles if the flag is enabled.
-        if (mFeatureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
+        if (mFeatureFlags.getPipelineHostEnabled()) {
             return;
         }
         if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt
index 1f63f5d..b2111d7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.qs.dagger
 
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QSHostAdapter
 import com.android.systemui.qs.QSTileHost
@@ -27,6 +25,7 @@
 import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedSharedPrefsRepository
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractorImpl
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -45,11 +44,11 @@
         @Provides
         @JvmStatic
         fun providePanelInteractor(
-            featureFlags: FeatureFlags,
+            featureFlags: QSPipelineFlagsRepository,
             qsHost: QSTileHost,
             panelInteractorImpl: PanelInteractorImpl
         ): PanelInteractor {
-            return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
+            return if (featureFlags.pipelineHostEnabled) {
                 panelInteractorImpl
             } else {
                 qsHost
@@ -59,11 +58,11 @@
         @Provides
         @JvmStatic
         fun provideCustomTileAddedRepository(
-            featureFlags: FeatureFlags,
+            featureFlags: QSPipelineFlagsRepository,
             qsHost: QSTileHost,
             customTileAddedRepository: CustomTileAddedSharedPrefsRepository
         ): CustomTileAddedRepository {
-            return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
+            return if (featureFlags.pipelineHostEnabled) {
                 customTileAddedRepository
             } else {
                 qsHost
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 966f370..5a5e47a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -27,8 +27,6 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.nano.SystemUIProtoDump
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.qs.QSFactory
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.external.CustomTile
@@ -39,6 +37,7 @@
 import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository
 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
 import com.android.systemui.qs.pipeline.domain.model.TileModel
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.qs.toProto
@@ -139,7 +138,7 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
     private val logger: QSPipelineLogger,
-    featureFlags: FeatureFlags,
+    featureFlags: QSPipelineFlagsRepository,
 ) : CurrentTilesInteractor {
 
     private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> =
@@ -171,7 +170,7 @@
         }
 
     init {
-        if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
+        if (featureFlags.pipelineHostEnabled) {
             startTileCollection()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
index 224fc1a..0743ba0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
@@ -18,10 +18,9 @@
 
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
 import javax.inject.Inject
 
 @SysUISingleton
@@ -30,14 +29,11 @@
 constructor(
     private val currentTilesInteractor: CurrentTilesInteractor,
     private val autoAddInteractor: AutoAddInteractor,
-    private val featureFlags: FeatureFlags,
+    private val featureFlags: QSPipelineFlagsRepository,
 ) : CoreStartable {
 
     override fun start() {
-        if (
-            featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) &&
-                featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)
-        ) {
+        if (featureFlags.pipelineAutoAddEnabled) {
             autoAddInteractor.init(currentTilesInteractor)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt
new file mode 100644
index 0000000..551b0f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt
@@ -0,0 +1,23 @@
+package com.android.systemui.qs.pipeline.shared
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+/** Encapsulate the different QS pipeline flags and their dependencies */
+@SysUISingleton
+class QSPipelineFlagsRepository
+@Inject
+constructor(
+    private val featureFlags: FeatureFlags,
+) {
+
+    /** @see Flags.QS_PIPELINE_NEW_HOST */
+    val pipelineHostEnabled: Boolean
+        get() = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)
+
+    /** @see Flags.QS_PIPELINE_AUTO_ADD */
+    val pipelineAutoAddEnabled: Boolean
+        get() = pipelineHostEnabled && featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index e8683fb..fb99775 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -63,6 +63,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setDialogTitle(R.string.screenrecord_permission_dialog_title)
+        setTitle(R.string.screenrecord_title)
         setStartButtonText(R.string.screenrecord_permission_dialog_continue)
         setStartButtonOnClickListener { v: View? ->
             onStartRecordingClicked?.run()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index df7d88f..014093d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -163,7 +163,6 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
-import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -356,7 +355,6 @@
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final KeyguardRootView mKeyguardRootView;
     private final QuickSettingsController mQsController;
-    private final ShadeInteractor mShadeInteractor;
     private final TouchHandler mTouchHandler = new TouchHandler();
 
     private long mDownTime;
@@ -561,6 +559,7 @@
     private float mHintDistance;
     private float mInitialOffsetOnTouch;
     private boolean mCollapsedAndHeadsUpOnDown;
+    private float mExpandedFraction = 0;
     private float mExpansionDragDownAmountPx = 0;
     private boolean mPanelClosedOnDown;
     private boolean mHasLayoutedSinceDown;
@@ -710,12 +709,10 @@
             VibratorHelper vibratorHelper,
             LatencyTracker latencyTracker,
             PowerManager powerManager,
-            AccessibilityManager accessibilityManager,
-            @DisplayId int displayId,
+            AccessibilityManager accessibilityManager, @DisplayId int displayId,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             MetricsLogger metricsLogger,
             ShadeLogger shadeLogger,
-            ShadeInteractor shadeInteractor,
             ConfigurationController configurationController,
             Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
@@ -788,7 +785,6 @@
         mView = view;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLockscreenGestureLogger = lockscreenGestureLogger;
-        mShadeInteractor = shadeInteractor;
         mShadeExpansionStateManager = shadeExpansionStateManager;
         mShadeLog = shadeLogger;
         mGutsManager = gutsManager;
@@ -989,6 +985,7 @@
         // Make sure the clock is in the correct position after the unlock animation
         // so that it's not in the wrong place when we show the keyguard again.
         positionClockAndNotifications(true /* forceClockUpdate */);
+        mScrimController.onUnlockAnimationFinished();
     }
 
     private void unlockAnimationStarted(
@@ -1247,6 +1244,13 @@
             mKeyguardStatusViewController.init();
         }
         mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
+        mKeyguardStatusViewController.getView().addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    int oldHeight = oldBottom - oldTop;
+                    if (v.getHeight() != oldHeight) {
+                        mNotificationStackScrollLayoutController.animateNextTopPaddingChange();
+                    }
+                });
 
         updateClockAppearance();
 
@@ -2248,7 +2252,7 @@
             if (!isFalseTouch(x, y, interactionType)) {
                 mShadeLog.logFlingExpands(vel, vectorVel, interactionType,
                         this.mFlingAnimationUtils.getMinVelocityPxPerSecond(),
-                        getExpandedFraction() > 0.5f, mAllowExpandForSmallExpansion);
+                        mExpandedFraction > 0.5f, mAllowExpandForSmallExpansion);
                 if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
                     expands = shouldExpandWhenNotFlinging();
                 } else {
@@ -2423,7 +2427,7 @@
     void requestScrollerTopPaddingUpdate(boolean animate) {
         mNotificationStackScrollLayoutController.updateTopPadding(
                 mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing,
-                        getKeyguardNotificationStaticPadding(), getExpandedFraction()), animate);
+                        getKeyguardNotificationStaticPadding(), mExpandedFraction), animate);
         if (isKeyguardShowing()
                 && mKeyguardBypassController.getBypassEnabled()) {
             // update the position of the header
@@ -2505,10 +2509,10 @@
     private void onHeightUpdated(float expandedHeight) {
         if (expandedHeight <= 0) {
             mShadeLog.logExpansionChanged("onHeightUpdated: fully collapsed.",
-                    getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                    mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
         } else if (isFullyExpanded()) {
             mShadeLog.logExpansionChanged("onHeightUpdated: fully expanded.",
-                    getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                    mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
         }
         if (!mQsController.getExpanded() || mQsController.isExpandImmediate()
                 || mIsExpandingOrCollapsing && mQsController.getExpandedWhenExpandingStarted()) {
@@ -3434,7 +3438,7 @@
         ipw.print("mHintDistance="); ipw.println(mHintDistance);
         ipw.print("mInitialOffsetOnTouch="); ipw.println(mInitialOffsetOnTouch);
         ipw.print("mCollapsedAndHeadsUpOnDown="); ipw.println(mCollapsedAndHeadsUpOnDown);
-        ipw.print("getExpandedFraction()="); ipw.println(getExpandedFraction());
+        ipw.print("mExpandedFraction="); ipw.println(mExpandedFraction);
         ipw.print("mExpansionDragDownAmountPx="); ipw.println(mExpansionDragDownAmountPx);
         ipw.print("mPanelClosedOnDown="); ipw.println(mPanelClosedOnDown);
         ipw.print("mHasLayoutedSinceDown="); ipw.println(mHasLayoutedSinceDown);
@@ -3770,7 +3774,7 @@
 
             // don't fling while in keyguard to avoid jump in shade expand animation;
             // touch has been intercepted already so flinging here is redundant
-            if (mBarState == KEYGUARD && getExpandedFraction() >= 1.0) {
+            if (mBarState == KEYGUARD && mExpandedFraction >= 1.0) {
                 mShadeLog.d("NPVC endMotionEvent - skipping fling on keyguard");
             } else {
                 fling(vel, expand, isFalseTouch(x, y, interactionType));
@@ -3866,16 +3870,6 @@
     @VisibleForTesting
     void setExpandedHeight(float height) {
         debugLog("setExpandedHeight(%.1f)", height);
-        int maxPanelTransitionDistance = getMaxPanelTransitionDistance();
-        if (maxPanelTransitionDistance == 0) {
-            setExpandedFracAndHeight(0, height);
-        } else {
-            setExpandedFracAndHeight(height / maxPanelTransitionDistance, height);
-        }
-    }
-
-    private void setExpandedFracAndHeight(float frac, float height) {
-        mShadeInteractor.setExpansion(frac);
         setExpandedHeightInternal(height);
     }
 
@@ -3931,9 +3925,11 @@
                     mHeightAnimator.end();
                 }
             }
-            mQsController.setShadeExpansion(mExpandedHeight, getExpandedFraction());
+            mExpandedFraction = Math.min(1f,
+                    maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
+            mQsController.setShadeExpansion(mExpandedHeight, mExpandedFraction);
             mExpansionDragDownAmountPx = h;
-            mAmbientState.setExpansionFraction(getExpandedFraction());
+            mAmbientState.setExpansionFraction(mExpandedFraction);
             onHeightUpdated(mExpandedHeight);
             updateExpansionAndVisibility();
         });
@@ -3961,7 +3957,8 @@
     /** Sets the expanded height relative to a number from 0 to 1. */
     @VisibleForTesting
     void setExpandedFraction(float frac) {
-        setExpandedFracAndHeight(frac, getMaxPanelTransitionDistance() * frac);
+        final int maxDist = getMaxPanelTransitionDistance();
+        setExpandedHeight(maxDist * frac);
     }
 
     float getExpandedHeight() {
@@ -3969,7 +3966,7 @@
     }
 
     float getExpandedFraction() {
-        return mShadeInteractor.getExpansion().getValue();
+        return mExpandedFraction;
     }
 
     @Override
@@ -3991,7 +3988,7 @@
 
     @Override
     public boolean isFullyCollapsed() {
-        return getExpandedFraction() <= 0.0f;
+        return mExpandedFraction <= 0.0f;
     }
 
     @Override
@@ -4150,7 +4147,7 @@
                                         animator.getAnimatedFraction()));
                         setOverExpansionInternal(expansion, false /* isFromGesture */);
                     }
-                    setExpandedHeight((float) animation.getAnimatedValue());
+                    setExpandedHeightInternal((float) animation.getAnimatedValue());
                 });
         return animator;
     }
@@ -4164,14 +4161,14 @@
     @Override
     public void updateExpansionAndVisibility() {
         mShadeExpansionStateManager.onPanelExpansionChanged(
-                getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
 
         updateVisibility();
     }
 
     @Override
     public boolean isExpanded() {
-        return getExpandedFraction() > 0f
+        return mExpandedFraction > 0f
                 || mInstantExpanding
                 || isPanelVisibleBecauseOfHeadsUp()
                 || mTracking
@@ -4929,7 +4926,7 @@
                     mMotionAborted = false;
                     mPanelClosedOnDown = isFullyCollapsed();
                     mShadeLog.logPanelClosedOnDown("intercept down touch", mPanelClosedOnDown,
-                            getExpandedFraction());
+                            mExpandedFraction);
                     mCollapsedAndHeadsUpOnDown = false;
                     mHasLayoutedSinceDown = false;
                     mUpdateFlingOnLayout = false;
@@ -5148,7 +5145,7 @@
                     mMinExpandHeight = 0.0f;
                     mPanelClosedOnDown = isFullyCollapsed();
                     mShadeLog.logPanelClosedOnDown("handle down touch", mPanelClosedOnDown,
-                            getExpandedFraction());
+                            mExpandedFraction);
                     mHasLayoutedSinceDown = false;
                     mUpdateFlingOnLayout = false;
                     mMotionAborted = false;
@@ -5207,7 +5204,7 @@
                     if (isFullyCollapsed()) {
                         // If panel is fully collapsed, reset haptic effect before adding movement.
                         mHasVibratedOnOpen = false;
-                        mShadeLog.logHasVibrated(mHasVibratedOnOpen, getExpandedFraction());
+                        mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction);
                     }
                     addMovement(event);
                     if (!isFullyCollapsed()) {
@@ -5243,7 +5240,7 @@
                         // otherwise {@link NotificationStackScrollLayout}
                         // wrongly enables stack height updates at the start of lockscreen swipe-up
                         mAmbientState.setSwipingUp(h <= 0);
-                        setExpandedHeight(newHeight);
+                        setExpandedHeightInternal(newHeight);
                     }
                     break;
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 832a25b..802ed80 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -21,8 +21,6 @@
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.app.StatusBarManager;
-import android.media.AudioManager;
-import android.media.session.MediaSessionLegacyHelper;
 import android.os.PowerManager;
 import android.util.Log;
 import android.view.GestureDetector;
@@ -47,6 +45,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -101,6 +100,7 @@
     private final NotificationInsetsController mNotificationInsetsController;
     private final boolean mIsTrackpadCommonEnabled;
     private final FeatureFlags mFeatureFlags;
+    private final KeyEventInteractor mKeyEventInteractor;
     private GestureDetector mPulsingWakeupGestureHandler;
     private GestureDetector mDreamingWakeupGestureHandler;
     private View mBrightnessMirror;
@@ -164,7 +164,8 @@
             FeatureFlags featureFlags,
             SystemClock clock,
             BouncerMessageInteractor bouncerMessageInteractor,
-            BouncerLogger bouncerLogger) {
+            BouncerLogger bouncerLogger,
+            KeyEventInteractor keyEventInteractor) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mStatusBarStateController = statusBarStateController;
@@ -190,6 +191,7 @@
         mNotificationInsetsController = notificationInsetsController;
         mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON);
         mFeatureFlags = featureFlags;
+        mKeyEventInteractor = keyEventInteractor;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -457,44 +459,17 @@
 
             @Override
             public boolean interceptMediaKey(KeyEvent event) {
-                return mService.interceptMediaKey(event);
+                return mKeyEventInteractor.interceptMediaKey(event);
             }
 
             @Override
             public boolean dispatchKeyEventPreIme(KeyEvent event) {
-                return mService.dispatchKeyEventPreIme(event);
+                return mKeyEventInteractor.dispatchKeyEventPreIme(event);
             }
 
             @Override
             public boolean dispatchKeyEvent(KeyEvent event) {
-                boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
-                switch (event.getKeyCode()) {
-                    case KeyEvent.KEYCODE_BACK:
-                        if (!down) {
-                            mBackActionInteractor.onBackRequested();
-                        }
-                        return true;
-                    case KeyEvent.KEYCODE_MENU:
-                        if (!down) {
-                            return mService.onMenuPressed();
-                        }
-                        break;
-                    case KeyEvent.KEYCODE_SPACE:
-                        if (!down) {
-                            return mService.onSpacePressed();
-                        }
-                        break;
-                    case KeyEvent.KEYCODE_VOLUME_DOWN:
-                    case KeyEvent.KEYCODE_VOLUME_UP:
-                        if (mStatusBarStateController.isDozing()) {
-                            MediaSessionLegacyHelper.getHelper(mView.getContext())
-                                    .sendVolumeKeyEvent(
-                                            event, AudioManager.USE_DEFAULT_STREAM_TYPE, true);
-                            return true;
-                        }
-                        break;
-                }
-                return false;
+                return mKeyEventInteractor.dispatchKeyEvent(event);
             }
         });
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index 76dca4c..ebb9935 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -34,34 +34,13 @@
     /** ShadeModel information regarding shade expansion events */
     val shadeModel: Flow<ShadeModel>
 
-    /**
-     * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully
-     * expanded and `0` is fully collapsed. Quick Settings can be expanded without a fully expanded
-     * shade.
-     */
+    /** Amount qs has expanded. Quick Settings can be expanded without the full shade expansion. */
     val qsExpansion: StateFlow<Float>
 
-    /**
-     * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully
-     * expanded and `0` is fully collapsed.
-     */
-    val expansion: StateFlow<Float>
-
     /** Amount shade has expanded with regard to the UDFPS location */
     val udfpsTransitionToFullShadeProgress: StateFlow<Float>
 
-    /**
-     * Set shade expansion to a value from `0` to `1` representing the amount the shade has expanded
-     * where `1` is fully expanded and `0` is fully collapsed.
-     */
-    fun setExpansion(expansion: Float)
-
-    /**
-     * Set quick settings expansion to a value from `0` to `1` representing the amount quick
-     * settings has expanded where `1` is fully expanded and `0` is fully collapsed.
-     */
     fun setQsExpansion(qsExpansion: Float)
-
     fun setUdfpsTransitionToFullShadeProgress(progress: Float)
 }
 
@@ -99,17 +78,9 @@
     private val _qsExpansion = MutableStateFlow(0f)
     override val qsExpansion: StateFlow<Float> = _qsExpansion.asStateFlow()
 
-    private val _expansion = MutableStateFlow(0f)
-    override val expansion: StateFlow<Float> = _expansion.asStateFlow()
-
     private var _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f)
     override val udfpsTransitionToFullShadeProgress: StateFlow<Float> =
         _udfpsTransitionToFullShadeProgress.asStateFlow()
-
-    override fun setExpansion(expansion: Float) {
-        _expansion.value = expansion
-    }
-
     override fun setQsExpansion(qsExpansion: Float) {
         _qsExpansion.value = qsExpansion
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index efe3eb4..6fde84a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -39,29 +38,18 @@
 @Inject
 constructor(
     @Application scope: CoroutineScope,
-    private val shadeRepository: ShadeRepository,
     disableFlagsRepository: DisableFlagsRepository,
     keyguardRepository: KeyguardRepository,
     userSetupRepository: UserSetupRepository,
     deviceProvisionedController: DeviceProvisionedController,
     userInteractor: UserInteractor,
 ) {
-    /**
-     * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully
-     * expanded and `0` is fully collapsed.
-     */
-    val expansion = shadeRepository.expansion
-
     /** Emits true if the shade is currently allowed and false otherwise. */
     val isShadeEnabled: StateFlow<Boolean> =
         disableFlagsRepository.disableFlags
             .map { it.isShadeEnabled() }
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
 
-    fun setExpansion(expansion: Float) {
-        shadeRepository.setExpansion(expansion)
-    }
-
     /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
     val isExpandToQsEnabled: Flow<Boolean> =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index d3c19b7..5042f1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -31,56 +31,68 @@
 ) {
     fun logInitialClick(
         entry: NotificationEntry?,
+        index: Integer?,
         pendingIntent: PendingIntent
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = entry?.key
             str2 = entry?.ranking?.channel?.id
-            str3 = pendingIntent.intent.toString()
+            str3 = pendingIntent.toString()
+            int1 = index?.toInt() ?: Int.MIN_VALUE
         }, {
-            "ACTION CLICK $str1 (channel=$str2) for pending intent $str3"
+            "ACTION CLICK $str1 (channel=$str2) for pending intent $str3 at index $int1"
         })
     }
 
     fun logRemoteInputWasHandled(
-        entry: NotificationEntry?
+        entry: NotificationEntry?,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = entry?.key
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Triggered remote input (for $str1))"
+            "  [Action click] Triggered remote input (for $str1) at index $int1"
         })
     }
 
     fun logStartingIntentWithDefaultHandler(
         entry: NotificationEntry?,
-        pendingIntent: PendingIntent
+        pendingIntent: PendingIntent,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = entry?.key
-            str2 = pendingIntent.intent.toString()
+            str2 = pendingIntent.toString()
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Launching intent $str2 via default handler (for $str1)"
+            "  [Action click] Launching intent $str2 via default handler (for $str1 at index $int1)"
         })
     }
 
     fun logWaitingToCloseKeyguard(
-        pendingIntent: PendingIntent
+        pendingIntent: PendingIntent,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = pendingIntent.intent.toString()
+            str1 = pendingIntent.toString()
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Intent $str1 launches an activity, dismissing keyguard first..."
+            "  [Action click] Intent $str1 at index $int1 launches an activity, dismissing " +
+                    "keyguard first..."
         })
     }
 
     fun logKeyguardGone(
-        pendingIntent: PendingIntent
+        pendingIntent: PendingIntent,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = pendingIntent.intent.toString()
+            str1 = pendingIntent.toString()
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Keyguard dismissed, calling default handler for intent $str1"
+            "  [Action click] Keyguard dismissed, calling default handler for intent $str1 at " +
+                    "index $int1"
         })
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt
index abf81c5..692a997 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt
@@ -2,10 +2,12 @@
 
 import android.app.Notification
 import android.os.RemoteException
+import android.util.Log
 import com.android.internal.statusbar.IStatusBarService
 import com.android.internal.statusbar.NotificationVisibility
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.util.Assert
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -21,7 +23,8 @@
 @SysUISingleton
 public class NotificationClickNotifier @Inject constructor(
     val barService: IStatusBarService,
-    @Main val mainExecutor: Executor
+    @Main val mainExecutor: Executor,
+    @UiBackground val backgroundExecutor: Executor
 ) {
     val listeners = mutableListOf<NotificationInteractionListener>()
 
@@ -48,13 +51,14 @@
         visibility: NotificationVisibility,
         generatedByAssistant: Boolean
     ) {
-        try {
-            barService.onNotificationActionClick(
-                    key, actionIndex, action, visibility, generatedByAssistant)
-        } catch (e: RemoteException) {
-            // nothing
+        backgroundExecutor.execute {
+            try {
+                barService.onNotificationActionClick(
+                        key, actionIndex, action, visibility, generatedByAssistant)
+            } catch (e: RemoteException) {
+                // nothing
+            }
         }
-
         mainExecutor.execute {
             notifyListenersAboutInteraction(key)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index da84afe..8089fd9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -119,11 +119,14 @@
             mPowerInteractor.wakeUpIfDozing(
                     "NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
 
+            Integer actionIndex = (Integer)
+                    view.getTag(com.android.internal.R.id.notification_action_index_tag);
+
             final NotificationEntry entry = getNotificationForParent(view.getParent());
-            mLogger.logInitialClick(entry, pendingIntent);
+            mLogger.logInitialClick(entry, actionIndex, pendingIntent);
 
             if (handleRemoteInput(view, pendingIntent)) {
-                mLogger.logRemoteInputWasHandled(entry);
+                mLogger.logRemoteInputWasHandled(entry, actionIndex);
                 return true;
             }
 
@@ -141,9 +144,9 @@
             }
             Notification.Action action = getActionFromView(view, entry, pendingIntent);
             return mCallback.handleRemoteViewClick(view, pendingIntent,
-                    action == null ? false : action.isAuthenticationRequired(), () -> {
+                    action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> {
                     Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
-                    mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent);
+                    mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent, actionIndex);
                     boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
                     if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
                     return started;
@@ -692,11 +695,13 @@
          * @param view
          * @param pendingIntent
          * @param appRequestedAuth
+         * @param actionIndex
          * @param defaultHandler
          * @return  true iff the click was handled
          */
         boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
-                boolean appRequestedAuth, ClickHandler defaultHandler);
+                boolean appRequestedAuth, @Nullable Integer actionIndex,
+                ClickHandler defaultHandler);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index d6a14604..6dd24ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -48,8 +48,10 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.jank.InteractionJankMonitor.Configuration;
 import com.android.internal.logging.UiEventLogger;
+import com.android.keyguard.KeyguardClockSwitch;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dumpable;
+import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
@@ -420,6 +422,25 @@
         }
     }
 
+    /** Returns the id of the currently rendering clock */
+    public String getClockId() {
+        if (mView == null) {
+            return KeyguardClockSwitch.MISSING_CLOCK_ID;
+        }
+
+        View clockSwitch = mView.findViewById(R.id.keyguard_clock_container);
+        if (clockSwitch == null) {
+            Log.e(TAG, "Clock container was missing");
+            return KeyguardClockSwitch.MISSING_CLOCK_ID;
+        }
+        if (!(clockSwitch instanceof KeyguardClockSwitch)) {
+            Log.e(TAG, "Clock container was incorrect type: " + clockSwitch);
+            return KeyguardClockSwitch.MISSING_CLOCK_ID;
+        }
+
+        return ((KeyguardClockSwitch) clockSwitch).getClockId();
+    }
+
     private void beginInteractionJankMonitor() {
         final boolean shouldPost =
                 (mIsDozing && mDozeAmount == 0) || (!mIsDozing && mDozeAmount == 1);
@@ -429,6 +450,7 @@
                         Choreographer.CALLBACK_ANIMATION, this::beginInteractionJankMonitor, null);
             } else {
                 Configuration.Builder builder = Configuration.Builder.withView(getCujType(), mView)
+                        .setTag(getClockId())
                         .setDeferMonitorForAnimationStart(false);
                 mInteractionJankMonitor.begin(builder);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
index ed80f33..5558ab1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
@@ -2,13 +2,17 @@
 
 # Bug component: 78010
 
-aaliomer@google.com
+aioana@google.com
 aroederer@google.com
+iyz@google.com
 jeffdq@google.com
 juliacr@google.com
 juliatuttle@google.com
+kurucz@google.com
+liuyining@google.com
 lynhan@google.com
-steell@google.com
+matiashe@google.com
+valiiftime@google.com
 yurilin@google.com
 
 per-file MediaNotificationProcessor.java = ethibodeau@google.com, asc@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
new file mode 100644
index 0000000..26dfe3e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
@@ -0,0 +1,684 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.notification.icon.ui.viewbinder
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Rect
+import android.os.Bundle
+import android.os.Trace
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.collection.ArrayMap
+import com.android.app.animation.Interpolators
+import com.android.internal.statusbar.StatusBarIcon
+import com.android.internal.util.ContrastColorUtil
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.ViewRefactorFlag
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.CrossFadeHelper
+import com.android.systemui.statusbar.NotificationListener
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.NotificationIconAreaController
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import java.util.function.Function
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+
+/**
+ * Controller class for [NotificationIconContainer]. This implementation serves as a temporary
+ * wrapper around [NotificationIconContainerViewBinder], so that external code can continue to
+ * depend on the [NotificationIconAreaController] interface. Once
+ * [LegacyNotificationIconAreaControllerImpl] is removed, this class can go away and the ViewBinder
+ * can be used directly.
+ */
+@SysUISingleton
+class NotificationIconAreaControllerViewBinderWrapperImpl
+@Inject
+constructor(
+    private val context: Context,
+    private val statusBarStateController: StatusBarStateController,
+    private val wakeUpCoordinator: NotificationWakeUpCoordinator,
+    private val bypassController: KeyguardBypassController,
+    private val mediaManager: NotificationMediaManager,
+    notificationListener: NotificationListener,
+    private val dozeParameters: DozeParameters,
+    private val sectionStyleProvider: SectionStyleProvider,
+    private val bubblesOptional: Optional<Bubbles>,
+    demoModeController: DemoModeController,
+    darkIconDispatcher: DarkIconDispatcher,
+    featureFlags: FeatureFlags,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val screenOffAnimationController: ScreenOffAnimationController,
+    private val shelfIconsViewModel: NotificationIconContainerShelfViewModel,
+    private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel,
+    private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
+) :
+    NotificationIconAreaController,
+    DarkIconDispatcher.DarkReceiver,
+    StatusBarStateController.StateListener,
+    NotificationWakeUpCoordinator.WakeUpListener,
+    DemoMode {
+
+    private val contrastColorUtil: ContrastColorUtil = ContrastColorUtil.getInstance(context)
+    private val updateStatusBarIcons = Runnable { updateStatusBarIcons() }
+    private val shelfRefactor = ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR)
+    private val tintAreas = ArrayList<Rect>()
+
+    private var iconSize = 0
+    private var iconHPadding = 0
+    private var iconTint = Color.WHITE
+    private var notificationEntries = listOf<ListEntry>()
+    private var notificationIconArea: View? = null
+    private var notificationIcons: NotificationIconContainer? = null
+    private var shelfIcons: NotificationIconContainer? = null
+    private var aodIcons: NotificationIconContainer? = null
+    private var aodBindJob: DisposableHandle? = null
+    private var aodIconAppearTranslation = 0
+    private var animationsEnabled = false
+    private var aodIconTint = 0
+    private var aodIconsVisible = false
+    private var showLowPriority = true
+
+    @VisibleForTesting
+    val settingsListener: NotificationListener.NotificationSettingsListener =
+        object : NotificationListener.NotificationSettingsListener {
+            override fun onStatusBarIconsBehaviorChanged(hideSilentStatusIcons: Boolean) {
+                showLowPriority = !hideSilentStatusIcons
+                updateStatusBarIcons()
+            }
+        }
+
+    init {
+        statusBarStateController.addCallback(this)
+        wakeUpCoordinator.addListener(this)
+        demoModeController.addCallback(this)
+        notificationListener.addNotificationSettingsListener(settingsListener)
+        initializeNotificationAreaViews(context)
+        reloadAodColor()
+        darkIconDispatcher.addDarkReceiver(this)
+    }
+
+    @VisibleForTesting
+    fun shouldShowLowPriorityIcons(): Boolean {
+        return showLowPriority
+    }
+
+    /** Called by the Keyguard*ViewController whose view contains the aod icons. */
+    override fun setupAodIcons(aodIcons: NotificationIconContainer) {
+        val changed = this.aodIcons != null && aodIcons !== this.aodIcons
+        if (changed) {
+            this.aodIcons!!.setAnimationsEnabled(false)
+            this.aodIcons!!.removeAllViews()
+            aodBindJob?.dispose()
+        }
+        this.aodIcons = aodIcons
+        this.aodIcons!!.setOnLockScreen(true)
+        aodBindJob = NotificationIconContainerViewBinder.bind(aodIcons, aodIconsViewModel)
+        updateAodIconsVisibility(animate = false, forceUpdate = changed)
+        updateAnimations()
+        if (changed) {
+            updateAodNotificationIcons()
+        }
+        updateIconLayoutParams(context)
+    }
+
+    override fun setupShelf(notificationShelfController: NotificationShelfController) =
+        NotificationShelfViewBinderWrapperControllerImpl.unsupported
+
+    override fun setShelfIcons(icons: NotificationIconContainer) {
+        if (shelfRefactor.expectEnabled()) {
+            NotificationIconContainerViewBinder.bind(icons, shelfIconsViewModel)
+            shelfIcons = icons
+        }
+    }
+
+    override fun onDensityOrFontScaleChanged(context: Context) {
+        updateIconLayoutParams(context)
+    }
+
+    /** Returns the view that represents the notification area. */
+    override fun getNotificationInnerAreaView(): View? {
+        return notificationIconArea
+    }
+
+    /**
+     * See [com.android.systemui.statusbar.policy.DarkIconDispatcher.setIconsDarkArea]. Sets the
+     * color that should be used to tint any icons in the notification area.
+     *
+     * @param tintAreas the areas in which to tint the icons, specified in screen coordinates
+     * @param darkIntensity
+     */
+    override fun onDarkChanged(tintAreas: ArrayList<Rect>, darkIntensity: Float, iconTint: Int) {
+        this.tintAreas.clear()
+        this.tintAreas.addAll(tintAreas)
+        if (DarkIconDispatcher.isInAreas(tintAreas, notificationIconArea)) {
+            this.iconTint = iconTint
+        }
+        applyNotificationIconsTint()
+    }
+
+    /** Updates the notifications with the given list of notifications to display. */
+    override fun updateNotificationIcons(entries: List<ListEntry>) {
+        notificationEntries = entries
+        updateNotificationIcons()
+    }
+
+    private fun updateStatusBarIcons() {
+        updateIconsForLayout(
+            { entry: NotificationEntry -> entry.icons.statusBarIcon },
+            notificationIcons,
+            showAmbient = false /* showAmbient */,
+            showLowPriority = showLowPriority,
+            hideDismissed = true /* hideDismissed */,
+            hideRepliedMessages = true /* hideRepliedMessages */,
+            hideCurrentMedia = false /* hideCurrentMedia */,
+            hidePulsing = false /* hidePulsing */
+        )
+    }
+
+    override fun updateAodNotificationIcons() {
+        if (aodIcons == null) {
+            return
+        }
+        updateIconsForLayout(
+            { entry: NotificationEntry -> entry.icons.aodIcon },
+            aodIcons,
+            showAmbient = false /* showAmbient */,
+            showLowPriority = true /* showLowPriority */,
+            hideDismissed = true /* hideDismissed */,
+            hideRepliedMessages = true /* hideRepliedMessages */,
+            hideCurrentMedia = true /* hideCurrentMedia */,
+            hidePulsing = bypassController.bypassEnabled /* hidePulsing */
+        )
+    }
+
+    override fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) {
+        notificationIcons!!.showIconIsolated(icon, animated)
+    }
+
+    override fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean) {
+        notificationIcons!!.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate)
+    }
+
+    override fun onDozingChanged(isDozing: Boolean) {
+        if (aodIcons == null) {
+            return
+        }
+        val animate = (dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking)
+        aodIcons!!.setDozing(isDozing, animate, 0)
+    }
+
+    override fun setAnimationsEnabled(enabled: Boolean) {
+        animationsEnabled = enabled
+        updateAnimations()
+    }
+
+    override fun onStateChanged(newState: Int) {
+        updateAodIconsVisibility(animate = false, forceUpdate = false)
+        updateAnimations()
+    }
+
+    override fun onThemeChanged() {
+        reloadAodColor()
+        updateAodIconColors()
+    }
+
+    override fun getHeight(): Int {
+        return if (aodIcons == null) 0 else aodIcons!!.height
+    }
+
+    @VisibleForTesting
+    fun appearAodIcons() {
+        if (aodIcons == null) {
+            return
+        }
+        if (screenOffAnimationController.shouldAnimateAodIcons()) {
+            aodIcons!!.translationY = -aodIconAppearTranslation.toFloat()
+            aodIcons!!.alpha = 0f
+            animateInAodIconTranslation()
+            aodIcons!!
+                .animate()
+                .alpha(1f)
+                .setInterpolator(Interpolators.LINEAR)
+                .setDuration(AOD_ICONS_APPEAR_DURATION)
+                .start()
+        } else {
+            aodIcons!!.alpha = 1.0f
+            aodIcons!!.translationY = 0f
+        }
+    }
+
+    override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
+        var animate = true
+        if (!bypassController.bypassEnabled) {
+            animate = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
+            // We only want the appear animations to happen when the notifications get fully hidden,
+            // since otherwise the unhide animation overlaps
+            animate = animate and isFullyHidden
+        }
+        updateAodIconsVisibility(animate, false /* force */)
+        updateAodNotificationIcons()
+        updateAodIconColors()
+    }
+
+    override fun onPulseExpansionChanged(expandingChanged: Boolean) {
+        if (expandingChanged) {
+            updateAodIconsVisibility(animate = true, forceUpdate = false)
+        }
+    }
+
+    override fun demoCommands(): List<String> {
+        val commands = ArrayList<String>()
+        commands.add(DemoMode.COMMAND_NOTIFICATIONS)
+        return commands
+    }
+
+    override fun dispatchDemoCommand(command: String, args: Bundle) {
+        if (notificationIconArea != null) {
+            val visible = args.getString("visible")
+            val vis = if ("false" == visible) View.INVISIBLE else View.VISIBLE
+            notificationIconArea?.visibility = vis
+        }
+    }
+
+    override fun onDemoModeFinished() {
+        if (notificationIconArea != null) {
+            notificationIconArea?.visibility = View.VISIBLE
+        }
+    }
+
+    private fun inflateIconArea(inflater: LayoutInflater): View {
+        return inflater.inflate(R.layout.notification_icon_area, null)
+    }
+
+    /** Initializes the views that will represent the notification area. */
+    private fun initializeNotificationAreaViews(context: Context) {
+        reloadDimens(context)
+        val layoutInflater = LayoutInflater.from(context)
+        notificationIconArea = inflateIconArea(layoutInflater)
+        notificationIcons = notificationIconArea?.findViewById(R.id.notificationIcons)
+        NotificationIconContainerViewBinder.bind(notificationIcons!!, statusBarIconsViewModel)
+    }
+
+    private fun updateIconLayoutParams(context: Context) {
+        reloadDimens(context)
+        val params = generateIconLayoutParams()
+        for (i in 0 until notificationIcons!!.childCount) {
+            val child = notificationIcons!!.getChildAt(i)
+            child.layoutParams = params
+        }
+        if (shelfIcons != null) {
+            for (i in 0 until shelfIcons!!.childCount) {
+                val child = shelfIcons!!.getChildAt(i)
+                child.layoutParams = params
+            }
+        }
+        if (aodIcons != null) {
+            for (i in 0 until aodIcons!!.childCount) {
+                val child = aodIcons!!.getChildAt(i)
+                child.layoutParams = params
+            }
+        }
+    }
+
+    private fun generateIconLayoutParams(): FrameLayout.LayoutParams {
+        return FrameLayout.LayoutParams(
+            iconSize + 2 * iconHPadding,
+            statusBarWindowController.statusBarHeight
+        )
+    }
+
+    private fun reloadDimens(context: Context) {
+        val res = context.resources
+        iconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size_sp)
+        iconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
+        aodIconAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation)
+    }
+
+    private fun shouldShowNotificationIcon(
+        entry: NotificationEntry,
+        showAmbient: Boolean,
+        showLowPriority: Boolean,
+        hideDismissed: Boolean,
+        hideRepliedMessages: Boolean,
+        hideCurrentMedia: Boolean,
+        hidePulsing: Boolean
+    ): Boolean {
+        if (!showAmbient && sectionStyleProvider.isMinimized(entry)) {
+            return false
+        }
+        if (hideCurrentMedia && entry.key == mediaManager.mediaNotificationKey) {
+            return false
+        }
+        if (!showLowPriority && sectionStyleProvider.isSilent(entry)) {
+            return false
+        }
+        if (entry.isRowDismissed && hideDismissed) {
+            return false
+        }
+        if (hideRepliedMessages && entry.isLastMessageFromReply) {
+            return false
+        }
+        // showAmbient == show in shade but not shelf
+        if (!showAmbient && entry.shouldSuppressStatusBar()) {
+            return false
+        }
+        if (
+            hidePulsing &&
+                entry.showingPulsing() &&
+                (!wakeUpCoordinator.notificationsFullyHidden || !entry.isPulseSuppressed)
+        ) {
+            return false
+        }
+        return if (bubblesOptional.isPresent && bubblesOptional.get().isBubbleExpanded(entry.key)) {
+            false
+        } else true
+    }
+
+    private fun updateNotificationIcons() {
+        Trace.beginSection("NotificationIconAreaController.updateNotificationIcons")
+        updateStatusBarIcons()
+        updateShelfIcons()
+        updateAodNotificationIcons()
+        applyNotificationIconsTint()
+        Trace.endSection()
+    }
+
+    private fun updateShelfIcons() {
+        if (shelfIcons == null) {
+            return
+        }
+        updateIconsForLayout(
+            { entry: NotificationEntry -> entry.icons.shelfIcon },
+            shelfIcons,
+            showAmbient = true,
+            showLowPriority = true,
+            hideDismissed = false,
+            hideRepliedMessages = false,
+            hideCurrentMedia = false,
+            hidePulsing = false
+        )
+    }
+
+    /**
+     * Updates the notification icons for a host layout. This will ensure that the notification host
+     * layout will have the same icons like the ones in here.
+     *
+     * @param function A function to look up an icon view based on an entry
+     * @param hostLayout which layout should be updated
+     * @param showAmbient should ambient notification icons be shown
+     * @param showLowPriority should icons from silent notifications be shown
+     * @param hideDismissed should dismissed icons be hidden
+     * @param hideRepliedMessages should messages that have been replied to be hidden
+     * @param hidePulsing should pulsing notifications be hidden
+     */
+    private fun updateIconsForLayout(
+        function: Function<NotificationEntry, StatusBarIconView?>,
+        hostLayout: NotificationIconContainer?,
+        showAmbient: Boolean,
+        showLowPriority: Boolean,
+        hideDismissed: Boolean,
+        hideRepliedMessages: Boolean,
+        hideCurrentMedia: Boolean,
+        hidePulsing: Boolean,
+    ) {
+        val toShow = ArrayList<StatusBarIconView>(notificationEntries.size)
+        // Filter out ambient notifications and notification children.
+        for (i in notificationEntries.indices) {
+            val entry = notificationEntries[i].representativeEntry
+            if (entry != null && entry.row != null) {
+                if (
+                    shouldShowNotificationIcon(
+                        entry,
+                        showAmbient,
+                        showLowPriority,
+                        hideDismissed,
+                        hideRepliedMessages,
+                        hideCurrentMedia,
+                        hidePulsing
+                    )
+                ) {
+                    val iconView = function.apply(entry)
+                    if (iconView != null) {
+                        toShow.add(iconView)
+                    }
+                }
+            }
+        }
+
+        // In case we are changing the suppression of a group, the replacement shouldn't flicker
+        // and it should just be replaced instead. We therefore look for notifications that were
+        // just replaced by the child or vice-versa to suppress this.
+        val replacingIcons = ArrayMap<String, ArrayList<StatusBarIcon>>()
+        val toRemove = ArrayList<View>()
+        for (i in 0 until hostLayout!!.childCount) {
+            val child = hostLayout.getChildAt(i) as? StatusBarIconView ?: continue
+            if (!toShow.contains(child)) {
+                var iconWasReplaced = false
+                val removedGroupKey = child.notification.groupKey
+                for (j in toShow.indices) {
+                    val candidate = toShow[j]
+                    if (
+                        candidate.sourceIcon.sameAs(child.sourceIcon) &&
+                            candidate.notification.groupKey == removedGroupKey
+                    ) {
+                        if (!iconWasReplaced) {
+                            iconWasReplaced = true
+                        } else {
+                            iconWasReplaced = false
+                            break
+                        }
+                    }
+                }
+                if (iconWasReplaced) {
+                    var statusBarIcons = replacingIcons[removedGroupKey]
+                    if (statusBarIcons == null) {
+                        statusBarIcons = ArrayList()
+                        replacingIcons[removedGroupKey] = statusBarIcons
+                    }
+                    statusBarIcons.add(child.statusBarIcon)
+                }
+                toRemove.add(child)
+            }
+        }
+        // removing all duplicates
+        val duplicates = ArrayList<String?>()
+        for (key in replacingIcons.keys) {
+            val statusBarIcons = replacingIcons[key]!!
+            if (statusBarIcons.size != 1) {
+                duplicates.add(key)
+            }
+        }
+        replacingIcons.removeAll(duplicates)
+        hostLayout.setReplacingIcons(replacingIcons)
+        val toRemoveCount = toRemove.size
+        for (i in 0 until toRemoveCount) {
+            hostLayout.removeView(toRemove[i])
+        }
+        val params = generateIconLayoutParams()
+        for (i in toShow.indices) {
+            val v = toShow[i]
+            // The view might still be transiently added if it was just removed and added again
+            hostLayout.removeTransientView(v)
+            if (v.parent == null) {
+                if (hideDismissed) {
+                    v.setOnDismissListener(updateStatusBarIcons)
+                }
+                hostLayout.addView(v, i, params)
+            }
+        }
+        hostLayout.setChangingViewPositions(true)
+        // Re-sort notification icons
+        val childCount = hostLayout.childCount
+        for (i in 0 until childCount) {
+            val actual = hostLayout.getChildAt(i)
+            val expected = toShow[i]
+            if (actual === expected) {
+                continue
+            }
+            hostLayout.removeView(expected)
+            hostLayout.addView(expected, i)
+        }
+        hostLayout.setChangingViewPositions(false)
+        hostLayout.setReplacingIcons(null)
+    }
+
+    /** Applies [.mIconTint] to the notification icons. */
+    private fun applyNotificationIconsTint() {
+        for (i in 0 until notificationIcons!!.childCount) {
+            val iv = notificationIcons!!.getChildAt(i) as StatusBarIconView
+            if (iv.width != 0) {
+                updateTintForIcon(iv, iconTint)
+            } else {
+                iv.executeOnLayout { updateTintForIcon(iv, iconTint) }
+            }
+        }
+        updateAodIconColors()
+    }
+
+    private fun updateTintForIcon(v: StatusBarIconView, tint: Int) {
+        val isPreL = java.lang.Boolean.TRUE == v.getTag(R.id.icon_is_pre_L)
+        var color = StatusBarIconView.NO_COLOR
+        val colorize = !isPreL || NotificationUtils.isGrayscale(v, contrastColorUtil)
+        if (colorize) {
+            color = DarkIconDispatcher.getTint(tintAreas, v, tint)
+        }
+        v.staticDrawableColor = color
+        v.setDecorColor(tint)
+    }
+
+    private fun updateAnimations() {
+        val inShade = statusBarStateController.state == StatusBarState.SHADE
+        if (aodIcons != null) {
+            aodIcons!!.setAnimationsEnabled(animationsEnabled && !inShade)
+        }
+        notificationIcons!!.setAnimationsEnabled(animationsEnabled && inShade)
+    }
+
+    private fun animateInAodIconTranslation() {
+        aodIcons!!
+            .animate()
+            .setInterpolator(Interpolators.DECELERATE_QUINT)
+            .translationY(0f)
+            .setDuration(AOD_ICONS_APPEAR_DURATION)
+            .start()
+    }
+
+    private fun reloadAodColor() {
+        aodIconTint =
+            Utils.getColorAttrDefaultColor(
+                context,
+                R.attr.wallpaperTextColor,
+                DEFAULT_AOD_ICON_COLOR
+            )
+    }
+
+    private fun updateAodIconColors() {
+        if (aodIcons != null) {
+            for (i in 0 until aodIcons!!.childCount) {
+                val iv = aodIcons!!.getChildAt(i) as StatusBarIconView
+                if (iv.width != 0) {
+                    updateTintForIcon(iv, aodIconTint)
+                } else {
+                    iv.executeOnLayout { updateTintForIcon(iv, aodIconTint) }
+                }
+            }
+        }
+    }
+
+    private fun updateAodIconsVisibility(animate: Boolean, forceUpdate: Boolean) {
+        if (aodIcons == null) {
+            return
+        }
+        var visible = (bypassController.bypassEnabled || wakeUpCoordinator.notificationsFullyHidden)
+
+        // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off animation is
+        // playing, in which case we want them to be visible since we're animating in the AOD UI and
+        // will be switching to KEYGUARD shortly.
+        if (
+            statusBarStateController.state != StatusBarState.KEYGUARD &&
+                !screenOffAnimationController.shouldShowAodIconsWhenShade()
+        ) {
+            visible = false
+        }
+        if (visible && wakeUpCoordinator.isPulseExpanding() && !bypassController.bypassEnabled) {
+            visible = false
+        }
+        if (aodIconsVisible != visible || forceUpdate) {
+            aodIconsVisible = visible
+            aodIcons!!.animate().cancel()
+            if (animate) {
+                val wasFullyInvisible = aodIcons!!.visibility != View.VISIBLE
+                if (aodIconsVisible) {
+                    if (wasFullyInvisible) {
+                        // No fading here, let's just appear the icons instead!
+                        aodIcons!!.visibility = View.VISIBLE
+                        aodIcons!!.alpha = 1.0f
+                        appearAodIcons()
+                    } else {
+                        // Let's make sure the icon are translated to 0, since we cancelled it above
+                        animateInAodIconTranslation()
+                        // We were fading out, let's fade in instead
+                        CrossFadeHelper.fadeIn(aodIcons)
+                    }
+                } else {
+                    // Let's make sure the icon are translated to 0, since we cancelled it above
+                    animateInAodIconTranslation()
+                    CrossFadeHelper.fadeOut(aodIcons)
+                }
+            } else {
+                aodIcons!!.alpha = 1.0f
+                aodIcons!!.translationY = 0f
+                aodIcons!!.visibility = if (visible) View.VISIBLE else View.INVISIBLE
+            }
+        }
+    }
+
+    companion object {
+        private const val AOD_ICONS_APPEAR_DURATION: Long = 200
+
+        @ColorInt private val DEFAULT_AOD_ICON_COLOR = -0x1
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
new file mode 100644
index 0000000..8293bb3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.notification.icon.ui.viewbinder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import kotlinx.coroutines.DisposableHandle
+
+/** Binds a [NotificationIconContainer] to its [view model][NotificationIconContainerViewModel]. */
+object NotificationIconContainerViewBinder {
+    fun bind(
+        view: NotificationIconContainer,
+        viewModel: NotificationIconContainerViewModel,
+    ): DisposableHandle {
+        return view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) {} }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
new file mode 100644
index 0000000..f68b0ef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.notification.icon.ui.viewmodel
+
+import javax.inject.Inject
+
+/** View-model for the row of notification icons displayed on the always-on display. */
+class NotificationIconContainerAlwaysOnDisplayViewModel @Inject constructor() :
+    NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
new file mode 100644
index 0000000..933c76f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.notification.icon.ui.viewmodel
+
+import javax.inject.Inject
+
+/** View-model for the overflow row of notification icons displayed in the notification shade. */
+class NotificationIconContainerShelfViewModel @Inject constructor() :
+    NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
new file mode 100644
index 0000000..2217646
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.notification.icon.ui.viewmodel
+
+import javax.inject.Inject
+
+/** View-model for the row of notification icons displayed in the status bar, */
+class NotificationIconContainerStatusBarViewModel @Inject constructor() :
+    NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
new file mode 100644
index 0000000..892b2be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.notification.icon.ui.viewmodel
+
+/**
+ * View-model for the row of notification icons displayed in the NotificationShelf, StatusBar, and
+ * AOD.
+ */
+interface NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt
index b002330..c276827 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt
@@ -20,7 +20,9 @@
 import android.util.AttributeSet
 import android.view.View
 import android.widget.TextView
+import com.android.internal.widget.ConversationLayout
 import com.android.internal.widget.ImageFloatingTextView
+import com.android.internal.widget.MessagingLayout
 import javax.inject.Inject
 
 class PrecomputedTextViewFactory @Inject constructor() : NotifRemoteViewsFactory {
@@ -35,6 +37,10 @@
             TextView::class.java.simpleName -> PrecomputedTextView(context, attrs)
             ImageFloatingTextView::class.java.name ->
                 PrecomputedImageFloatingTextView(context, attrs)
+            MessagingLayout::class.java.name ->
+                MessagingLayout(context, attrs).apply { setPrecomputedTextEnabled(true) }
+            ConversationLayout::class.java.name ->
+                ConversationLayout(context, attrs).apply { setPrecomputedTextEnabled(true) }
             else -> null
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 22a87a7..b92c51f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -64,8 +64,10 @@
 
     override fun setOnClickListener(listener: View.OnClickListener) = unsupported
 
-    private val unsupported: Nothing
-        get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled")
+    companion object {
+        val unsupported: Nothing
+            get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled")
+    }
 }
 
 /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 4668aa4..6db8df9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -28,7 +28,6 @@
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows;
-import static com.android.systemui.statusbar.phone.NotificationIconAreaController.HIGH_PRIORITY;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.content.res.Configuration;
@@ -150,6 +149,7 @@
 public class NotificationStackScrollLayoutController {
     private static final String TAG = "StackScrollerController";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
+    private static final String HIGH_PRIORITY = "high_priority";
 
     private final boolean mAllowLongPress;
     private final NotificationGutsManager mNotificationGutsManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 5c28be3..af09bf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -25,7 +25,6 @@
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.UserHandle;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationAdapter;
 import android.view.View;
@@ -276,19 +275,11 @@
 
     void userActivity();
 
-    boolean interceptMediaKey(KeyEvent event);
-
-    boolean dispatchKeyEventPreIme(KeyEvent event);
-
-    boolean onMenuPressed();
-
     void endAffordanceLaunch();
 
     /** Should the keyguard be hidden immediately in response to a back press/gesture. */
     boolean shouldKeyguardHideImmediately();
 
-    boolean onSpacePressed();
-
     void showBouncerWithDimissAndCancelIfKeyguard(OnDismissAction performAction,
             Runnable cancelAction);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 8ffd43a..ccb87bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -89,7 +89,6 @@
 import android.view.Display;
 import android.view.IRemoteAnimationRunner;
 import android.view.IWindowManager;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ThreadedRenderer;
 import android.view.View;
@@ -2636,44 +2635,6 @@
     }
 
     @Override
-    public boolean interceptMediaKey(KeyEvent event) {
-        return mState == StatusBarState.KEYGUARD
-                && mStatusBarKeyguardViewManager.interceptMediaKey(event);
-    }
-
-    /**
-     * While IME is active and a BACK event is detected, check with
-     * {@link StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event
-     * should be handled before routing to IME, in order to prevent the user having to hit back
-     * twice to exit bouncer.
-     */
-    @Override
-    public boolean dispatchKeyEventPreIme(KeyEvent event) {
-        switch (event.getKeyCode()) {
-            case KeyEvent.KEYCODE_BACK:
-                if (mState == StatusBarState.KEYGUARD
-                        && mStatusBarKeyguardViewManager.dispatchBackKeyEventPreIme()) {
-                    return mBackActionInteractor.onBackRequested();
-                }
-        }
-        return false;
-    }
-
-    protected boolean shouldUnlockOnMenuPressed() {
-        return mDeviceInteractive && mState != StatusBarState.SHADE
-            && mStatusBarKeyguardViewManager.shouldDismissOnMenuPressed();
-    }
-
-    @Override
-    public boolean onMenuPressed() {
-        if (shouldUnlockOnMenuPressed()) {
-            mShadeController.animateCollapseShadeForced();
-            return true;
-        }
-        return false;
-    }
-
-    @Override
     public void endAffordanceLaunch() {
         releaseGestureWakeLock();
         mCameraLauncherLazy.get().setLaunchingAffordance(false);
@@ -2692,15 +2653,6 @@
         return (isScrimmedBouncer || isBouncerOverDream);
     }
 
-    @Override
-    public boolean onSpacePressed() {
-        if (mDeviceInteractive && mState != StatusBarState.SHADE) {
-            mShadeController.animateCollapseShadeForced();
-            return true;
-        }
-        return false;
-    }
-
     private void showBouncerOrLockScreenIfKeyguard() {
         // If the keyguard is animating away, we aren't really the keyguard anymore and should not
         // show the bouncer/lockscreen.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index 0bf0f4b..d22ed38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.phone;
 
 import android.content.Context;
@@ -43,6 +58,8 @@
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.wm.shell.bubbles.Bubbles;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -55,13 +72,13 @@
  * normally reserved for notifications.
  */
 @SysUISingleton
-public class NotificationIconAreaController implements
+public class LegacyNotificationIconAreaControllerImpl implements
+        NotificationIconAreaController,
         DarkReceiver,
         StatusBarStateController.StateListener,
         NotificationWakeUpCoordinator.WakeUpListener,
         DemoMode {
 
-    public static final String HIGH_PRIORITY = "high_priority";
     private static final long AOD_ICONS_APPEAR_DURATION = 200;
     @ColorInt
     private static final int DEFAULT_AOD_ICON_COLOR = 0xffffffff;
@@ -110,7 +127,7 @@
             };
 
     @Inject
-    public NotificationIconAreaController(
+    public LegacyNotificationIconAreaControllerImpl(
             Context context,
             StatusBarStateController statusBarStateController,
             NotificationWakeUpCoordinator wakeUpCoordinator,
@@ -192,7 +209,7 @@
         }
     }
 
-    public void onDensityOrFontScaleChanged(Context context) {
+    public void onDensityOrFontScaleChanged(@NotNull Context context) {
         updateIconLayoutParams(context);
     }
 
@@ -493,7 +510,7 @@
         mNotificationIcons.showIconIsolated(icon, animated);
     }
 
-    public void setIsolatedIconLocation(Rect iconDrawingRect, boolean requireStateUpdate) {
+    public void setIsolatedIconLocation(@NotNull Rect iconDrawingRect, boolean requireStateUpdate) {
         mNotificationIcons.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt
new file mode 100644
index 0000000..0079f7c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.phone
+
+import android.content.Context
+import android.graphics.Rect
+import android.view.View
+import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+/**
+ * A controller for the space in the status bar to the left of the system icons. This area is
+ * normally reserved for notifications.
+ */
+interface NotificationIconAreaController {
+    /** Called by the Keyguard*ViewController whose view contains the aod icons. */
+    fun setupAodIcons(aodIcons: NotificationIconContainer)
+    fun setupShelf(notificationShelfController: NotificationShelfController)
+    fun setShelfIcons(icons: NotificationIconContainer)
+    fun onDensityOrFontScaleChanged(context: Context)
+
+    /** Returns the view that represents the notification area. */
+    fun getNotificationInnerAreaView(): View?
+
+    /** Updates the notifications with the given list of notifications to display. */
+    fun updateNotificationIcons(entries: List<@JvmSuppressWildcards ListEntry>)
+    fun updateAodNotificationIcons()
+    fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean)
+    fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean)
+    fun setAnimationsEnabled(enabled: Boolean)
+    fun onThemeChanged()
+    fun getHeight(): Int
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt
new file mode 100644
index 0000000..d1ddd51
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.phone
+
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconAreaControllerViewBinderWrapperImpl
+import dagger.Module
+import dagger.Provides
+import javax.inject.Provider
+
+@Module
+object NotificationIconAreaControllerModule {
+    @Provides
+    fun provideNotificationIconAreaControllerImpl(
+        featureFlags: FeatureFlags,
+        legacyProvider: Provider<LegacyNotificationIconAreaControllerImpl>,
+        newProvider: Provider<NotificationIconAreaControllerViewBinderWrapperImpl>,
+    ): NotificationIconAreaController =
+        if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            newProvider.get()
+        } else {
+            legacyProvider.get()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index fc66138..62a8cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -709,6 +709,11 @@
         }
     }
 
+    public void onUnlockAnimationFinished() {
+        mAnimatingPanelExpansionOnUnlock = false;
+        applyAndDispatchState();
+    }
+
     /**
      * Set the amount of progress we are currently in if we're transitioning to the full shade.
      * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 7fe01825..a6284e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -32,6 +32,8 @@
 import android.view.View;
 import android.view.ViewParent;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -254,16 +256,16 @@
 
     @Override
     public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
-            boolean appRequestedAuth,
+            boolean appRequestedAuth, @Nullable Integer actionIndex,
             NotificationRemoteInputManager.ClickHandler defaultHandler) {
         final boolean isActivity = pendingIntent.isActivity();
         if (isActivity || appRequestedAuth) {
-            mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent);
+            mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent, actionIndex);
             final boolean afterKeyguardGone = mActivityIntentHelper
                     .wouldPendingLaunchResolverActivity(pendingIntent,
                             mLockscreenUserManager.getCurrentUserId());
             mActivityStarter.dismissKeyguardThenExecute(() -> {
-                mActionClickLogger.logKeyguardGone(pendingIntent);
+                mActionClickLogger.logKeyguardGone(pendingIntent, actionIndex);
 
                 try {
                     ActivityManager.getService().resumeAppSwitches();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 1bceb29..cd1afc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -223,10 +223,14 @@
                 }
                 .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
             true /* animate */)
-        interactionJankMonitor.begin(
-                notifShadeWindowControllerLazy.get().windowRootView,
-            CUJ_SCREEN_OFF_SHOW_AOD
-        )
+        val builder = InteractionJankMonitor.Configuration.Builder
+            .withView(
+                    InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD,
+                    notifShadeWindowControllerLazy.get().windowRootView
+            )
+            .setTag(statusBarStateControllerImpl.getClockId())
+
+        interactionJankMonitor.begin(builder)
     }
 
     override fun onStartedWakingUp() {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index ad4bd58..dff058c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -621,6 +621,51 @@
         configurationListenerArgumentCaptor.value.onUiModeChanged()
         verify(view).reloadColors()
     }
+    @Test
+    fun onOrientationChanged_landscapeKeyguardFlagDisabled_blockReinflate() {
+        featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
+
+        // Run onOrientationChanged
+        val configurationListenerArgumentCaptor =
+            ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+        underTest.onViewAttached()
+        verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture())
+        clearInvocations(viewFlipperController)
+        configurationListenerArgumentCaptor.value.onOrientationChanged(
+            Configuration.ORIENTATION_LANDSCAPE
+        )
+        // Verify view is reinflated when flag is on
+        verify(viewFlipperController, never()).clearViews()
+        verify(viewFlipperController, never())
+            .asynchronouslyInflateView(
+                eq(SecurityMode.PIN),
+                any(),
+                onViewInflatedCallbackArgumentCaptor.capture()
+            )
+    }
+
+    @Test
+    fun onOrientationChanged_landscapeKeyguardFlagEnabled_doesReinflate() {
+        featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, true)
+
+        // Run onOrientationChanged
+        val configurationListenerArgumentCaptor =
+            ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+        underTest.onViewAttached()
+        verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture())
+        clearInvocations(viewFlipperController)
+        configurationListenerArgumentCaptor.value.onOrientationChanged(
+            Configuration.ORIENTATION_LANDSCAPE
+        )
+        // Verify view is reinflated when flag is on
+        verify(viewFlipperController).clearViews()
+        verify(viewFlipperController)
+            .asynchronouslyInflateView(
+                eq(SecurityMode.PIN),
+                any(),
+                onViewInflatedCallbackArgumentCaptor.capture()
+            )
+    }
 
     @Test
     fun hasDismissActions() {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index 7d23c80..b8b0198 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -77,7 +77,7 @@
     public void updatePosition_primaryClockAnimation() {
         ClockController mockClock = mock(ClockController.class);
         when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock);
-        when(mockClock.getConfig()).thenReturn(new ClockConfig(false, true));
+        when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", false, true));
 
         mController.updatePosition(10, 15, 20f, true);
 
@@ -92,7 +92,7 @@
     public void updatePosition_alternateClockAnimation() {
         ClockController mockClock = mock(ClockController.class);
         when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock);
-        when(mockClock.getConfig()).thenReturn(new ClockConfig(true, true));
+        when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", true, true));
 
         mController.updatePosition(10, 15, 20f, true);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index dcaafe8..6fcf54c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.graphics.Point
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
@@ -47,7 +47,7 @@
 @SmallTest
 @RoboPilotTest
 @OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class LightRevealScrimRepositoryTest : SysuiTestCase() {
     private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
     private lateinit var underTest: LightRevealScrimRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
index a3f7fc5..e0ae0c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -131,14 +131,12 @@
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() {
+    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_showsPrimaryBouncer() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
@@ -147,42 +145,48 @@
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
-
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() {
+    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_doNothing() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(asleepWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
+        verifyActionsDoNothing(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
-    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() {
+    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_showsPrimaryBouncer() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
 
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_SPACE)
+    }
 
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+    @Test
+    fun dispatchKeyEvent_spaceActionUp_shadeLocked_collapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_SPACE)
+    }
+
+    @Test
+    fun dispatchKeyEvent_enterActionUp_interactiveKeyguard_showsPrimaryBouncer() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_ENTER)
+    }
+
+    @Test
+    fun dispatchKeyEvent_enterActionUp_shadeLocked_collapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_ENTER)
     }
 
     @Test
@@ -252,4 +256,42 @@
             .isFalse()
         verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any())
     }
+
+    private fun verifyActionUpCollapsesTheShade(keycode: Int) {
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
+    }
+
+    private fun verifyActionUpShowsPrimaryBouncer(keycode: Int) {
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true))
+    }
+
+    private fun verifyActionsDoNothing(keycode: Int) {
+        // action down: does nothing
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+
+        // action up: doesNothing
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index ef38d54..3576ec9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -281,11 +281,7 @@
             underTest.onPreviewSlotSelected(
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
             )
-            keyguardInteractor.previewMode.value =
-                KeyguardInteractor.PreviewMode(
-                    isInPreviewMode = true,
-                    shouldHighlightSelectedAffordance = true,
-                )
+            underTest.enablePreviewMode(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, true)
 
             repository.setKeyguardShowing(false)
             val latest = collectLastValue(underTest.startButton)
@@ -330,11 +326,7 @@
             underTest.onPreviewSlotSelected(
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
             )
-            keyguardInteractor.previewMode.value =
-                KeyguardInteractor.PreviewMode(
-                    isInPreviewMode = true,
-                    shouldHighlightSelectedAffordance = true,
-                )
+            underTest.enablePreviewMode(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, true)
 
             repository.setKeyguardShowing(false)
             val endButton = collectLastValue(underTest.endButton)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 05e933b..a14a1c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -45,7 +45,6 @@
     private lateinit var testScope: TestScope
     private lateinit var repository: FakeKeyguardRepository
     private lateinit var keyguardInteractor: KeyguardInteractor
-    @Mock private lateinit var keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
 
     @Before
     fun setUp() {
@@ -63,10 +62,7 @@
         keyguardInteractor = withDeps.keyguardInteractor
         repository = withDeps.repository
 
-        underTest = KeyguardRootViewModel(
-            keyguardInteractor,
-            keyguardQuickAffordancesCombinedViewModel,
-        )
+        underTest = KeyguardRootViewModel(keyguardInteractor)
     }
 
     @Test
@@ -89,10 +85,7 @@
     fun alpha_inPreviewMode_doesNotChange() =
         testScope.runTest {
             val value = collectLastValue(underTest.alpha)
-            underTest.enablePreviewMode(
-                initiallySelectedSlotId = null,
-                shouldHighlightSelectedAffordance = false,
-            )
+            underTest.enablePreviewMode()
 
             Truth.assertThat(value()).isEqualTo(1f)
             repository.setKeyguardAlpha(0.1f)
@@ -104,4 +97,4 @@
             repository.setKeyguardAlpha(0f)
             Truth.assertThat(value()).isEqualTo(1f)
         }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 9781baa..64d3b82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -53,7 +53,6 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.PluginManager;
@@ -65,6 +64,7 @@
 import com.android.systemui.qs.external.TileLifecycleManager;
 import com.android.systemui.qs.external.TileServiceKey;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
@@ -131,6 +131,8 @@
 
     private FakeFeatureFlags mFeatureFlags;
 
+    private QSPipelineFlagsRepository mQSPipelineFlagsRepository;
+
     private FakeExecutor mMainExecutor;
 
     private QSTileHost mQSTileHost;
@@ -142,6 +144,7 @@
 
         mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false);
         mFeatureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, false);
+        mQSPipelineFlagsRepository = new QSPipelineFlagsRepository(mFeatureFlags);
 
         mMainExecutor = new FakeExecutor(new FakeSystemClock());
 
@@ -164,7 +167,7 @@
         mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor,
                 mPluginManager, mTunerService, () -> mAutoTiles, mShadeController,
                 mQSLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister,
-                mTileLifecycleManagerFactory, mUserFileManager, mFeatureFlags);
+                mTileLifecycleManagerFactory, mUserFileManager, mQSPipelineFlagsRepository);
         mMainExecutor.runAllReady();
 
         mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) {
@@ -708,7 +711,7 @@
                 UserTracker userTracker, SecureSettings secureSettings,
                 CustomTileStatePersister customTileStatePersister,
                 TileLifecycleManager.Factory tileLifecycleManagerFactory,
-                UserFileManager userFileManager, FeatureFlags featureFlags) {
+                UserFileManager userFileManager, QSPipelineFlagsRepository featureFlags) {
             super(context, defaultFactory, mainExecutor, pluginManager,
                     tunerService, autoTiles,  shadeController, qsLogger,
                     userTracker, secureSettings, customTileStatePersister,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 6689514..54a9360 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository
 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
 import com.android.systemui.qs.pipeline.domain.model.TileModel
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.qs.toProto
@@ -79,6 +80,7 @@
     private val customTileAddedRepository: CustomTileAddedRepository =
         FakeCustomTileAddedRepository()
     private val featureFlags = FakeFeatureFlags()
+    private val pipelineFlags = QSPipelineFlagsRepository(featureFlags)
     private val tileLifecycleManagerFactory = TLMFactory()
 
     @Mock private lateinit var customTileStatePersister: CustomTileStatePersister
@@ -120,7 +122,7 @@
                 backgroundDispatcher = testDispatcher,
                 scope = testScope.backgroundScope,
                 logger = logger,
-                featureFlags = featureFlags,
+                featureFlags = pipelineFlags,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
new file mode 100644
index 0000000..489221e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
@@ -0,0 +1,61 @@
+package com.android.systemui.qs.pipeline.shared
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.junit.runners.Parameterized.Parameters
+
+@SmallTest
+@RunWith(Parameterized::class)
+class QSPipelineFlagsRepositoryTest : SysuiTestCase() {
+    companion object {
+        @Parameters(
+            name =
+                """
+WHEN: qs_pipeline_new_host = {0}, qs_pipeline_auto_add = {1}
+THEN: pipelineNewHost = {2}, pipelineAutoAdd = {3}
+                """
+        )
+        @JvmStatic
+        fun data(): List<Array<Boolean>> =
+            (0 until 4).map { combination ->
+                val qs_pipeline_new_host = combination and 0b10 != 0
+                val qs_pipeline_auto_add = combination and 0b01 != 0
+                arrayOf(
+                    qs_pipeline_new_host,
+                    qs_pipeline_auto_add,
+                    /* pipelineNewHost = */ qs_pipeline_new_host,
+                    /* pipelineAutoAdd = */ qs_pipeline_new_host && qs_pipeline_auto_add,
+                )
+            }
+    }
+
+    @JvmField @Parameter(0) var qsPipelineNewHostFlag: Boolean = false
+    @JvmField @Parameter(1) var qsPipelineAutoAddFlag: Boolean = false
+    @JvmField @Parameter(2) var pipelineNewHostExpected: Boolean = false
+    @JvmField @Parameter(3) var pipelineAutoAddExpected: Boolean = false
+
+    private val featureFlags = FakeFeatureFlags()
+    private val pipelineFlags = QSPipelineFlagsRepository(featureFlags)
+
+    @Before
+    fun setUp() {
+        featureFlags.apply {
+            set(Flags.QS_PIPELINE_NEW_HOST, qsPipelineNewHostFlag)
+            set(Flags.QS_PIPELINE_AUTO_ADD, qsPipelineAutoAddFlag)
+        }
+    }
+
+    @Test
+    fun flagLogic() {
+        assertThat(pipelineFlags.pipelineHostEnabled).isEqualTo(pipelineNewHostExpected)
+        assertThat(pipelineFlags.pipelineAutoAddEnabled).isEqualTo(pipelineAutoAddExpected)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 5cad9f8..981e44b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -73,7 +73,6 @@
 import com.android.keyguard.KeyguardStatusViewController;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.LockIconViewController;
-import com.android.keyguard.TestScopeProvider;
 import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent;
 import com.android.keyguard.dagger.KeyguardStatusBarViewComponent;
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
@@ -120,7 +119,6 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
-import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -136,7 +134,6 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
@@ -167,17 +164,14 @@
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
 import com.android.systemui.statusbar.phone.TapAgainViewController;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
-import com.android.systemui.user.domain.interactor.UserInteractor;
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
@@ -364,16 +358,6 @@
         mFakeKeyguardRepository = keyguardInteractorDeps.getRepository();
         mKeyguardBottomAreaInteractor = new KeyguardBottomAreaInteractor(mFakeKeyguardRepository);
         mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor();
-        mShadeRepository = new FakeShadeRepository();
-        mShadeInteractor = new ShadeInteractor(
-                TestScopeProvider.getTestScope(),
-                mShadeRepository,
-                new FakeDisableFlagsRepository(),
-                new FakeKeyguardRepository(),
-                new FakeUserSetupRepository(),
-                mock(DeviceProvisionedController.class),
-                mock(UserInteractor.class)
-        );
 
         SystemClock systemClock = new FakeSystemClock();
         mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
@@ -600,33 +584,19 @@
                 mMainHandler,
                 mLayoutInflater,
                 mFeatureFlags,
-                coordinator,
-                expansionHandler,
-                mDynamicPrivacyController,
-                mKeyguardBypassController,
-                mFalsingManager,
-                new FalsingCollectorFake(),
+                coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController,
+                mFalsingManager, new FalsingCollectorFake(),
                 mKeyguardStateController,
                 mStatusBarStateController,
                 mStatusBarWindowStateController,
                 mNotificationShadeWindowController,
-                mDozeLog,
-                mDozeParameters,
-                mCommandQueue,
-                mVibratorHelper,
-                mLatencyTracker,
-                mPowerManager,
-                mAccessibilityManager,
-                0,
-                mUpdateMonitor,
+                mDozeLog, mDozeParameters, mCommandQueue, mVibratorHelper,
+                mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor,
                 mMetricsLogger,
                 mShadeLog,
-                mShadeInteractor,
                 mConfigurationController,
-                () -> flingAnimationUtilsBuilder,
-                mStatusBarTouchableRegionManager,
-                mConversationNotificationManager,
-                mMediaHierarchyManager,
+                () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
+                mConversationNotificationManager, mMediaHierarchyManager,
                 mStatusBarKeyguardViewManager,
                 mGutsManager,
                 mNotificationsQSContainerController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index eb4ae1a..7aeafeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -539,6 +539,23 @@
     }
 
     @Test
+    public void onKeyguardStatusViewHeightChange_animatesNextTopPaddingChangeForNSSL() {
+        ArgumentCaptor<View.OnLayoutChangeListener> captor =
+                ArgumentCaptor.forClass(View.OnLayoutChangeListener.class);
+        verify(mKeyguardStatusView).addOnLayoutChangeListener(captor.capture());
+        View.OnLayoutChangeListener listener = captor.getValue();
+
+        clearInvocations(mNotificationStackScrollLayoutController);
+
+        when(mKeyguardStatusView.getHeight()).thenReturn(0);
+        listener.onLayoutChange(mKeyguardStatusView, /* left= */ 0, /* top= */ 0, /* right= */
+                0, /* bottom= */ 0, /* oldLeft= */ 0, /* oldTop= */ 0, /* oldRight= */
+                0, /* oldBottom = */ 200);
+
+        verify(mNotificationStackScrollLayoutController).animateNextTopPaddingChange();
+    }
+
+    @Test
     public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
         mStatusBarStateController.setState(KEYGUARD);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 1edeeff..dc506a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -18,6 +18,7 @@
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
@@ -38,6 +39,7 @@
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -118,6 +120,7 @@
     @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock
     lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
+    @Mock lateinit var keyEventInteractor: KeyEventInteractor
     private val notificationExpansionRepository = NotificationExpansionRepository()
 
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
@@ -188,7 +191,8 @@
                         CountDownTimerUtil(),
                         featureFlags
                     ),
-                    BouncerLogger(logcatLogBuffer("BouncerLog"))
+                    BouncerLogger(logcatLogBuffer("BouncerLog")),
+                    keyEventInteractor,
             )
         underTest.setupExpandedStatusBar()
 
@@ -345,6 +349,27 @@
             verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area)
         }
 
+    @Test
+    fun forwardsDispatchKeyEvent() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B)
+        interactionEventHandler.dispatchKeyEvent(keyEvent)
+        verify(keyEventInteractor).dispatchKeyEvent(keyEvent)
+    }
+
+    @Test
+    fun forwardsDispatchKeyEventPreIme() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B)
+        interactionEventHandler.dispatchKeyEventPreIme(keyEvent)
+        verify(keyEventInteractor).dispatchKeyEventPreIme(keyEvent)
+    }
+
+    @Test
+    fun forwardsInterceptMediaKey() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP)
+        interactionEventHandler.interceptMediaKey(keyEvent)
+        verify(keyEventInteractor).interceptMediaKey(keyEvent)
+    }
+
     companion object {
         private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
         private const val VIEW_BOTTOM = 100
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 829184c..66d48d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
@@ -198,7 +199,8 @@
                     CountDownTimerUtil(),
                     featureFlags
                 ),
-                BouncerLogger(logcatLogBuffer("BouncerLog"))
+                BouncerLogger(logcatLogBuffer("BouncerLog")),
+                Mockito.mock(KeyEventInteractor::class.java),
             )
 
         controller.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 35a54d1..a2c2912 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -49,7 +49,6 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
-import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -171,7 +170,6 @@
         mShadeInteractor =
                 new ShadeInteractor(
                         mTestScope.getBackgroundScope(),
-                        new FakeShadeRepository(),
                         mDisableFlagsRepository,
                         mKeyguardRepository,
                         new FakeUserSetupRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
index 9e6c12f..cdcd1a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
@@ -67,7 +67,6 @@
     private val featureFlags = FakeFeatureFlags()
     private val userSetupRepository = FakeUserSetupRepository()
     private val userRepository = FakeUserRepository()
-    private val shadeRepository = FakeShadeRepository()
     private val disableFlagsRepository = FakeDisableFlagsRepository()
     private val keyguardRepository = FakeKeyguardRepository()
 
@@ -139,7 +138,6 @@
         underTest =
             ShadeInteractor(
                 testScope.backgroundScope,
-                shadeRepository,
                 disableFlagsRepository,
                 keyguardRepository,
                 userSetupRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index 9495fdd..ecaf137 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -227,6 +227,25 @@
     }
 
     @Test
+    fun activeClockId_changeAfterPluginConnected() {
+        val plugin1 = FakeClockPlugin()
+            .addClock("clock_1", "clock 1")
+            .addClock("clock_2", "clock 2")
+
+        val plugin2 = FakeClockPlugin()
+            .addClock("clock_3", "clock 3", { mockClock })
+            .addClock("clock_4", "clock 4")
+
+        registry.applySettings(ClockSettings("clock_3", null))
+
+        pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
+        assertEquals(DEFAULT_CLOCK_ID, registry.activeClockId)
+
+        pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
+        assertEquals("clock_3", registry.activeClockId)
+    }
+
+    @Test
     fun createDefaultClock_pluginDisconnected() {
         val plugin1 = FakeClockPlugin()
             .addClock("clock_1", "clock 1")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index ec4367c..4a2518a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -102,10 +102,8 @@
     @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback
     private val disableFlagsRepository = FakeDisableFlagsRepository()
     private val keyguardRepository = FakeKeyguardRepository()
-    private val shadeRepository = FakeShadeRepository()
     private val shadeInteractor = ShadeInteractor(
         testScope.backgroundScope,
-        shadeRepository,
         disableFlagsRepository,
         keyguardRepository,
         userSetupRepository = FakeUserSetupRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
new file mode 100644
index 0000000..b8792a8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 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.systemui.statusbar.notification.icon.ui.viewbinder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.NotificationListener
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+class NotificationIconAreaControllerViewBinderWrapperImplTest : SysuiTestCase() {
+    @Mock private lateinit var notifListener: NotificationListener
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var wakeUpCoordinator: NotificationWakeUpCoordinator
+    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+    @Mock private lateinit var notifMediaManager: NotificationMediaManager
+    @Mock private lateinit var dozeParams: DozeParameters
+    @Mock private lateinit var sectionStyleProvider: SectionStyleProvider
+    @Mock private lateinit var darkIconDispatcher: DarkIconDispatcher
+    @Mock private lateinit var statusBarWindowController: StatusBarWindowController
+    @Mock private lateinit var screenOffAnimController: ScreenOffAnimationController
+    @Mock private lateinit var bubbles: Bubbles
+    @Mock private lateinit var demoModeController: DemoModeController
+    @Mock private lateinit var aodIcons: NotificationIconContainer
+    @Mock private lateinit var featureFlags: FeatureFlags
+
+    private val shelfViewModel = NotificationIconContainerShelfViewModel()
+    private val statusBarViewModel = NotificationIconContainerStatusBarViewModel()
+    private val aodViewModel = NotificationIconContainerAlwaysOnDisplayViewModel()
+
+    private lateinit var underTest: NotificationIconAreaControllerViewBinderWrapperImpl
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            NotificationIconAreaControllerViewBinderWrapperImpl(
+                mContext,
+                statusBarStateController,
+                wakeUpCoordinator,
+                keyguardBypassController,
+                notifMediaManager,
+                notifListener,
+                dozeParams,
+                sectionStyleProvider,
+                Optional.of(bubbles),
+                demoModeController,
+                darkIconDispatcher,
+                featureFlags,
+                statusBarWindowController,
+                screenOffAnimController,
+                shelfViewModel,
+                statusBarViewModel,
+                aodViewModel,
+            )
+    }
+
+    @Test
+    fun testNotificationIcons_settingHideIcons() {
+        underTest.settingsListener.onStatusBarIconsBehaviorChanged(true)
+        assertFalse(underTest.shouldShowLowPriorityIcons())
+    }
+
+    @Test
+    fun testNotificationIcons_settingShowIcons() {
+        underTest.settingsListener.onStatusBarIconsBehaviorChanged(false)
+        assertTrue(underTest.shouldShowLowPriorityIcons())
+    }
+
+    @Test
+    fun testAppearResetsTranslation() {
+        underTest.setupAodIcons(aodIcons)
+        whenever(dozeParams.shouldControlScreenOff()).thenReturn(false)
+        underTest.appearAodIcons()
+        verify(aodIcons).translationY = 0f
+        verify(aodIcons).alpha = 1.0f
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
index 8e1dcf0..1b8cfd4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
@@ -48,7 +48,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class NotificationIconAreaControllerTest extends SysuiTestCase {
+public class LegacyNotificationIconAreaControllerImplTest extends SysuiTestCase {
 
     @Mock
     private NotificationListener mListener;
@@ -70,7 +70,7 @@
     StatusBarWindowController mStatusBarWindowController;
     @Mock
     ScreenOffAnimationController mScreenOffAnimationController;
-    private NotificationIconAreaController mController;
+    private LegacyNotificationIconAreaControllerImpl mController;
     @Mock
     private Bubbles mBubbles;
     @Mock private DemoModeController mDemoModeController;
@@ -82,7 +82,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mController = new NotificationIconAreaController(
+        mController = new LegacyNotificationIconAreaControllerImpl(
                 mContext,
                 mStatusBarStateController,
                 mWakeUpCoordinator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 0dc1d9a..6b3bd22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -1802,6 +1802,15 @@
         assertFalse(ScrimState.UNLOCKED.mAnimateChange);
     }
 
+    @Test
+    public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() {
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true);
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        mScrimController.onUnlockAnimationFinished();
+        assertAlphaAfterExpansion(mNotificationsScrim, 1f, 1f);
+    }
+
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
         mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 5f0011b..fdc4f37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -32,7 +32,6 @@
 import static org.junit.Assume.assumeNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -500,123 +499,45 @@
 
     @Test
     public void ifPortraitHalfOpen_drawVerticallyTop() {
-        DevicePostureController devicePostureController = mock(DevicePostureController.class);
-        when(devicePostureController.getDevicePosture())
-                .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
-
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                devicePostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0 , null);
-
-        verify(devicePostureController).addCallback(any());
-        dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
+        mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
         mTestableLooper.processAllMessages(); // let dismiss() finish
 
         setOrientation(Configuration.ORIENTATION_PORTRAIT);
 
         // Call show() to trigger layout updates before verifying position
-        dialog.show(SHOW_REASON_UNKNOWN);
+        mDialog.show(SHOW_REASON_UNKNOWN);
         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
 
-        int gravity = dialog.getWindowGravity();
+        int gravity = mDialog.getWindowGravity();
         assertEquals(Gravity.TOP, gravity & Gravity.VERTICAL_GRAVITY_MASK);
-
-        cleanUp(dialog);
     }
 
     @Test
     public void ifPortraitAndOpen_drawCenterVertically() {
-        DevicePostureController devicePostureController = mock(DevicePostureController.class);
-        when(devicePostureController.getDevicePosture())
-                .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
-
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                devicePostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0, null);
-
-        verify(devicePostureController).addCallback(any());
-        dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED);
+        mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED);
         mTestableLooper.processAllMessages(); // let dismiss() finish
 
         setOrientation(Configuration.ORIENTATION_PORTRAIT);
 
-        dialog.show(SHOW_REASON_UNKNOWN);
+        mDialog.show(SHOW_REASON_UNKNOWN);
         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
 
-        int gravity = dialog.getWindowGravity();
+        int gravity = mDialog.getWindowGravity();
         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
-
-        cleanUp(dialog);
     }
 
     @Test
     public void ifLandscapeAndHalfOpen_drawCenterVertically() {
-        DevicePostureController devicePostureController = mock(DevicePostureController.class);
-        when(devicePostureController.getDevicePosture())
-                .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
-
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                devicePostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0, null);
-
-        verify(devicePostureController).addCallback(any());
-        dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
+        mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
         mTestableLooper.processAllMessages(); // let dismiss() finish
 
         setOrientation(Configuration.ORIENTATION_LANDSCAPE);
 
-        dialog.show(SHOW_REASON_UNKNOWN);
+        mDialog.show(SHOW_REASON_UNKNOWN);
         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
 
-        int gravity = dialog.getWindowGravity();
+        int gravity = mDialog.getWindowGravity();
         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
-
-        cleanUp(dialog);
     }
 
     @Test
@@ -627,31 +548,9 @@
 
     @Test
     public void dialogDestroy_removesPostureControllerCallback() {
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                mPostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0, null);
-
         verify(mPostureController, never()).removeCallback(any());
-        dialog.destroy();
-
+        mDialog.destroy();
         verify(mPostureController).removeCallback(any());
-
-        cleanUp(dialog);
     }
 
     private void setOrientation(int orientation) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 35a3fd0..492e542 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -30,9 +30,6 @@
     private val _qsExpansion = MutableStateFlow(0f)
     override val qsExpansion = _qsExpansion
 
-    private val _expansion = MutableStateFlow(0f)
-    override val expansion = _expansion
-
     private val _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f)
     override val udfpsTransitionToFullShadeProgress = _udfpsTransitionToFullShadeProgress
 
@@ -44,10 +41,6 @@
         _qsExpansion.value = qsExpansion
     }
 
-    override fun setExpansion(expansion: Float) {
-        _expansion.value = expansion
-    }
-
     override fun setUdfpsTransitionToFullShadeProgress(progress: Float) {
         _udfpsTransitionToFullShadeProgress.value = progress
     }
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index ee6c28e..d1be5a9 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -43,6 +43,7 @@
 import android.companion.virtual.audio.IAudioRoutingCallback;
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorEvent;
+import android.content.AttributionSource;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -125,6 +126,7 @@
     private final VirtualDeviceManagerService mService;
     private final PendingTrampolineCallback mPendingTrampolineCallback;
     private final int mOwnerUid;
+    private final String mOwnerPackageName;
     private int mDeviceId;
     private @Nullable String mPersistentDeviceId;
     // Thou shall not hold the mVirtualDeviceLock over the mInputController calls.
@@ -196,7 +198,7 @@
             AssociationInfo associationInfo,
             VirtualDeviceManagerService service,
             IBinder token,
-            int ownerUid,
+            AttributionSource attributionSource,
             int deviceId,
             CameraAccessController cameraAccessController,
             PendingTrampolineCallback pendingTrampolineCallback,
@@ -209,7 +211,7 @@
                 associationInfo,
                 service,
                 token,
-                ownerUid,
+                attributionSource,
                 deviceId,
                 /* inputController= */ null,
                 cameraAccessController,
@@ -227,7 +229,7 @@
             AssociationInfo associationInfo,
             VirtualDeviceManagerService service,
             IBinder token,
-            int ownerUid,
+            AttributionSource attributionSource,
             int deviceId,
             InputController inputController,
             CameraAccessController cameraAccessController,
@@ -238,7 +240,8 @@
             VirtualDeviceParams params,
             DisplayManagerGlobal displayManager) {
         super(PermissionEnforcer.fromContext(context));
-        UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(ownerUid);
+        mOwnerPackageName = attributionSource.getPackageName();
+        UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(attributionSource.getUid());
         mContext = context.createContextAsUser(ownerUserHandle, 0);
         mAssociationInfo = associationInfo;
         mPersistentDeviceId = PERSISTENT_ID_PREFIX_CDM_ASSOCIATION + associationInfo.getId();
@@ -247,7 +250,7 @@
         mActivityListener = activityListener;
         mSoundEffectListener = soundEffectListener;
         mRunningAppsChangedCallback = runningAppsChangedCallback;
-        mOwnerUid = ownerUid;
+        mOwnerUid = attributionSource.getUid();
         mDeviceId = deviceId;
         mAppToken = token;
         mParams = params;
@@ -771,7 +774,9 @@
         fout.println("  VirtualDevice: ");
         fout.println("    mDeviceId: " + mDeviceId);
         fout.println("    mAssociationId: " + mAssociationInfo.getId());
-        fout.println("    mParams: " + mParams);
+        fout.println("    mOwnerPackageName: " + mOwnerPackageName);
+        fout.println("    mParams: ");
+        mParams.dump(fout, "        ");
         fout.println("    mVirtualDisplayIds: ");
         synchronized (mVirtualDeviceLock) {
             for (int i = 0; i < mVirtualDisplays.size(); i++) {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 77508a8..e558498 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -35,6 +35,7 @@
 import android.companion.virtual.VirtualDeviceManager;
 import android.companion.virtual.VirtualDeviceParams;
 import android.companion.virtual.sensor.VirtualSensor;
+import android.content.AttributionSource;
 import android.content.Context;
 import android.content.Intent;
 import android.hardware.display.IVirtualDisplayCallback;
@@ -314,13 +315,15 @@
         @Override // Binder call
         public IVirtualDevice createVirtualDevice(
                 IBinder token,
-                String packageName,
+                AttributionSource attributionSource,
                 int associationId,
                 @NonNull VirtualDeviceParams params,
                 @NonNull IVirtualDeviceActivityListener activityListener,
                 @NonNull IVirtualDeviceSoundEffectListener soundEffectListener) {
             createVirtualDevice_enforcePermission();
+            attributionSource.enforceCallingUid();
             final int callingUid = getCallingUid();
+            final String packageName = attributionSource.getPackageName();
             if (!PermissionUtils.validateCallingPackageName(getContext(), packageName)) {
                 throw new SecurityException(
                         "Package name " + packageName + " does not belong to calling uid "
@@ -340,10 +343,9 @@
             final int deviceId = sNextUniqueIndex.getAndIncrement();
             final Consumer<ArraySet<Integer>> runningAppsChangedCallback =
                     runningUids -> notifyRunningAppsChanged(deviceId, runningUids);
-            VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(),
-                    associationInfo, VirtualDeviceManagerService.this, token, callingUid,
-                    deviceId, cameraAccessController,
-                    mPendingTrampolineCallback, activityListener,
+            VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(), associationInfo,
+                    VirtualDeviceManagerService.this, token, attributionSource, deviceId,
+                    cameraAccessController, mPendingTrampolineCallback, activityListener,
                     soundEffectListener, runningAppsChangedCallback, params);
             synchronized (mVirtualDeviceManagerLock) {
                 if (mVirtualDevices.size() == 0) {
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 315972c..f594170 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -1225,15 +1225,13 @@
         public ContentCaptureOptions getOptions(@UserIdInt int userId,
                 @NonNull String packageName) {
             boolean isContentCaptureReceiverEnabled;
-            boolean isContentProtectionReceiverEnabled;
+            boolean isContentProtectionReceiverEnabled =
+                    isContentProtectionReceiverEnabled(userId, packageName);
             ArraySet<ComponentName> whitelistedComponents = null;
 
             synchronized (mGlobalWhitelistStateLock) {
                 isContentCaptureReceiverEnabled =
                         isContentCaptureReceiverEnabled(userId, packageName);
-                isContentProtectionReceiverEnabled =
-                        isContentProtectionReceiverEnabled(userId, packageName);
-
                 if (!isContentCaptureReceiverEnabled) {
                     // Full package is not allowlisted: check individual components next
                     whitelistedComponents = getWhitelistedComponents(userId, packageName);
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index b67e627..d47a399 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -81,7 +81,7 @@
 # when a notification has been clicked
 27520 notification_clicked (key|3),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1)
 # when a notification action button has been clicked
-27521 notification_action_clicked (key|3),(action_index|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1)
+27521 notification_action_clicked (key|3),(piIdentifier|3),(pendingIntent|3),(action_index|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1)
 # when a notification has been canceled
 27530 notification_canceled (key|3),(reason|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1),(listener|3)
 # replaces 27510 with a row per notification
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 0e4465d..1d09dce 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -87,7 +87,6 @@
         DeviceConfig.NAMESPACE_CAMERA_NATIVE,
         DeviceConfig.NAMESPACE_CONFIGURATION,
         DeviceConfig.NAMESPACE_CONNECTIVITY,
-        DeviceConfig.NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL,
         DeviceConfig.NAMESPACE_EDGETPU_NATIVE,
         DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
         DeviceConfig.NAMESPACE_INTELLIGENCE_CONTENT_SUGGESTIONS,
@@ -115,19 +114,29 @@
         NAMESPACE_TETHERING_U_OR_LATER_NATIVE
     };
 
+    // All the aconfig flags under the listed DeviceConfig scopes will be synced to native level.
+    @VisibleForTesting
+    static final String[] sDeviceConfigAconfigScopes = new String[] {
+        DeviceConfig.NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL,
+    };
+
     private final String[] mGlobalSettings;
 
     private final String[] mDeviceConfigScopes;
 
+    private final String[] mDeviceConfigAconfigScopes;
+
     private final ContentResolver mContentResolver;
 
     @VisibleForTesting
     protected SettingsToPropertiesMapper(ContentResolver contentResolver,
             String[] globalSettings,
-            String[] deviceConfigScopes) {
+            String[] deviceConfigScopes,
+            String[] deviceConfigAconfigScopes) {
         mContentResolver = contentResolver;
         mGlobalSettings = globalSettings;
         mDeviceConfigScopes = deviceConfigScopes;
+        mDeviceConfigAconfigScopes = deviceConfigAconfigScopes;
     }
 
     @VisibleForTesting
@@ -173,6 +182,36 @@
                                 return;
                             }
                             setProperty(propertyName, properties.getString(key, null));
+
+                            // for legacy namespaces, they can also be used for trunk stable
+                            // purposes. so push flag also into trunk stable slot in sys prop,
+                            // later all legacy usage will be refactored and the sync to old
+                            // sys prop slot can be removed.
+                            String aconfigPropertyName = makeAconfigFlagPropertyName(scope, key);
+                            if (aconfigPropertyName == null) {
+                                log("unable to construct system property for " + scope + "/"
+                                        + key);
+                                return;
+                            }
+                            setProperty(aconfigPropertyName, properties.getString(key, null));
+                        }
+                    });
+        }
+
+        for (String deviceConfigAconfigScope : mDeviceConfigAconfigScopes) {
+            DeviceConfig.addOnPropertiesChangedListener(
+                    deviceConfigAconfigScope,
+                    AsyncTask.THREAD_POOL_EXECUTOR,
+                    (DeviceConfig.Properties properties) -> {
+                        String scope = properties.getNamespace();
+                        for (String key : properties.getKeyset()) {
+                            String aconfigPropertyName = makeAconfigFlagPropertyName(scope, key);
+                            if (aconfigPropertyName == null) {
+                                log("unable to construct system property for " + scope + "/"
+                                        + key);
+                                return;
+                            }
+                            setProperty(aconfigPropertyName, properties.getString(key, null));
                         }
                     });
         }
@@ -180,7 +219,10 @@
 
     public static SettingsToPropertiesMapper start(ContentResolver contentResolver) {
         SettingsToPropertiesMapper mapper =  new SettingsToPropertiesMapper(
-                contentResolver, sGlobalSettings, sDeviceConfigScopes);
+                contentResolver,
+                sGlobalSettings,
+                sDeviceConfigScopes,
+                sDeviceConfigAconfigScopes);
         mapper.updatePropertiesFromSettings();
         return mapper;
     }
@@ -243,6 +285,28 @@
         return propertyName;
     }
 
+    /**
+     * system property name constructing rule for aconfig flags:
+     * "persist.device_config.aconfig_flags.[category_name].[flag_name]".
+     * If the name contains invalid characters or substrings for system property name,
+     * will return null.
+     * @param categoryName
+     * @param flagName
+     * @return
+     */
+    @VisibleForTesting
+    static String makeAconfigFlagPropertyName(String categoryName, String flagName) {
+        String propertyName = SYSTEM_PROPERTY_PREFIX + "aconfig_flags." +
+                              categoryName + "." + flagName;
+
+        if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX)
+                || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) {
+            return null;
+        }
+
+        return propertyName;
+    }
+
     private void setProperty(String key, String value) {
         // Check if need to clear the property
         if (value == null) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index d89171d..4e01997 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -391,7 +391,6 @@
         final boolean wasBtScoRequested = isBluetoothScoRequested();
         CommunicationRouteClient client;
 
-
         // Save previous client route in case of failure to start BT SCO audio
         AudioDeviceAttributes prevClientDevice = null;
         boolean prevPrivileged = false;
@@ -1043,7 +1042,7 @@
         synchronized (mBluetoothAudioStateLock) {
             mBluetoothScoOn = on;
             updateAudioHalBluetoothState();
-            postUpdateCommunicationRouteClient(eventSource);
+            postUpdateCommunicationRouteClient(isBluetoothScoRequested(), eventSource);
         }
     }
 
@@ -1395,8 +1394,10 @@
                 MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset);
     }
 
-    /*package*/ void postUpdateCommunicationRouteClient(String eventSource) {
-        sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, eventSource);
+    /*package*/ void postUpdateCommunicationRouteClient(
+            boolean wasBtScoRequested, String eventSource) {
+        sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE,
+                wasBtScoRequested ? 1 : 0, eventSource);
     }
 
     /*package*/ void postSetCommunicationDeviceForClient(CommunicationDeviceInfo info) {
@@ -1708,7 +1709,8 @@
                                             : AudioSystem.STREAM_DEFAULT);
                             if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
                                     || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
-                                onUpdateCommunicationRouteClient("setBluetoothActiveDevice");
+                                onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
+                                        "setBluetoothActiveDevice");
                             }
                         }
                     }
@@ -1762,9 +1764,11 @@
                 case MSG_I_SET_MODE_OWNER:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
+                            boolean wasBtScoRequested = isBluetoothScoRequested();
                             mAudioModeOwner = (AudioModeInfo) msg.obj;
                             if (mAudioModeOwner.mMode != AudioSystem.MODE_RINGTONE) {
-                                onUpdateCommunicationRouteClient("setNewModeOwner");
+                                onUpdateCommunicationRouteClient(
+                                        wasBtScoRequested, "setNewModeOwner");
                             }
                         }
                     }
@@ -1787,10 +1791,10 @@
                     }
                     break;
 
-                case MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT:
+                case MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
-                            onUpdateCommunicationRouteClient((String) msg.obj);
+                            onUpdateCommunicationRouteClient(msg.arg1 == 1, (String) msg.obj);
                         }
                     }
                     break;
@@ -1971,7 +1975,7 @@
     private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38;
 
     private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42;
-    private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
+    private static final int MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
     private static final int MSG_I_SCO_AUDIO_STATE_CHANGED = 44;
 
     private static final int MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT = 45;
@@ -2328,16 +2332,20 @@
      */
     // @GuardedBy("mSetModeLock")
     @GuardedBy("mDeviceStateLock")
-    private void onUpdateCommunicationRouteClient(String eventSource) {
-        updateCommunicationRoute(eventSource);
+    private void onUpdateCommunicationRouteClient(boolean wasBtScoRequested, String eventSource) {
         CommunicationRouteClient crc = topCommunicationRouteClient();
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "onUpdateCommunicationRouteClient, crc: "
-                    + crc + " eventSource: " + eventSource);
+            Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " + crc
+                    + " wasBtScoRequested: " + wasBtScoRequested + " eventSource: " + eventSource);
         }
         if (crc != null) {
             setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(),
                     BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource);
+        } else {
+            if (!isBluetoothScoRequested() && wasBtScoRequested) {
+                mBtHelper.stopBluetoothSco(eventSource);
+            }
+            updateCommunicationRoute(eventSource);
         }
     }
 
@@ -2431,6 +2439,7 @@
             List<AudioRecordingConfiguration> recordConfigs) {
         synchronized (mSetModeLock) {
             synchronized (mDeviceStateLock) {
+                final boolean wasBtScoRequested = isBluetoothScoRequested();
                 boolean updateCommunicationRoute = false;
                 for (CommunicationRouteClient crc : mCommunicationRouteClients) {
                     boolean wasActive = crc.isActive();
@@ -2459,7 +2468,8 @@
                     }
                 }
                 if (updateCommunicationRoute) {
-                    postUpdateCommunicationRouteClient("updateCommunicationRouteClientsActivity");
+                    postUpdateCommunicationRouteClient(
+                            wasBtScoRequested, "updateCommunicationRouteClientsActivity");
                 }
             }
         }
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index aaf5292..b350363 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -329,7 +329,7 @@
             default:
                 break;
         }
-        if(broadcast) {
+        if (broadcast) {
             broadcastScoConnectionState(scoAudioState);
             //FIXME: this is to maintain compatibility with deprecated intent
             // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
@@ -723,8 +723,10 @@
         checkScoAudioState();
         if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
             // Make sure that the state transitions to CONNECTING even if we cannot initiate
-            // the connection.
-            broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
+            // the connection except if already connected internally
+            if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL) {
+                broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
+            }
             switch (mScoAudioState) {
                 case SCO_STATE_INACTIVE:
                     mScoAudioMode = scoAudioMode;
@@ -780,7 +782,7 @@
                     broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED);
                     break;
                 case SCO_STATE_ACTIVE_INTERNAL:
-                    Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return");
+                    // Already in ACTIVE mode, simply return
                     break;
                 case SCO_STATE_ACTIVE_EXTERNAL:
                     /* Confirm SCO Audio connection to requesting app as it is already connected
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index d57dc47..8642fb8 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -16,6 +16,9 @@
 
 package com.android.server.display;
 
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Point;
@@ -132,12 +135,15 @@
     /**
      * Returns the default size of the surface associated with the display, or null if the surface
      * is not provided for layer mirroring by SurfaceFlinger. For non virtual displays, this will
-     * be the actual display device's size.
+     * be the actual display device's size, reflecting the current rotation.
      */
     @Nullable
     public Point getDisplaySurfaceDefaultSizeLocked() {
         DisplayDeviceInfo displayDeviceInfo = getDisplayDeviceInfoLocked();
-        return new Point(displayDeviceInfo.width, displayDeviceInfo.height);
+        final boolean isRotated = mCurrentOrientation == ROTATION_90
+                || mCurrentOrientation == ROTATION_270;
+        return isRotated ? new Point(displayDeviceInfo.height, displayDeviceInfo.width)
+                : new Point(displayDeviceInfo.width, displayDeviceInfo.height);
     }
 
     /**
@@ -358,7 +364,7 @@
         }
 
         boolean isRotated = (mCurrentOrientation == Surface.ROTATION_90
-                || mCurrentOrientation == Surface.ROTATION_270);
+                || mCurrentOrientation == ROTATION_270);
         DisplayDeviceInfo info = getDisplayDeviceInfoLocked();
         viewport.deviceWidth = isRotated ? info.height : info.width;
         viewport.deviceHeight = isRotated ? info.width : info.height;
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index c131226..01eceda 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -2093,7 +2093,7 @@
 
     /** Loads the refresh rate profiles. */
     private void loadRefreshRateZoneProfiles(RefreshRateConfigs refreshRateConfigs) {
-        if (refreshRateConfigs == null) {
+        if (refreshRateConfigs == null || refreshRateConfigs.getRefreshRateZoneProfiles() == null) {
             return;
         }
         for (RefreshRateZone zone :
diff --git a/services/core/java/com/android/server/input/FocusEventDebugView.java b/services/core/java/com/android/server/input/FocusEventDebugView.java
index 02e0eb0..4b8fabde 100644
--- a/services/core/java/com/android/server/input/FocusEventDebugView.java
+++ b/services/core/java/com/android/server/input/FocusEventDebugView.java
@@ -24,11 +24,13 @@
 import android.annotation.AnyThread;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
 import android.graphics.Typeface;
-import android.util.Log;
+import android.util.DisplayMetrics;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.TypedValue;
@@ -38,22 +40,29 @@
 import android.view.MotionEvent;
 import android.view.RoundedCorner;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.WindowInsets;
 import android.view.animation.AccelerateInterpolator;
 import android.widget.HorizontalScrollView;
 import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 
 /**
  *  Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on
  *  the screen.
  */
-class FocusEventDebugView extends LinearLayout {
+class FocusEventDebugView extends RelativeLayout {
 
     private static final String TAG = FocusEventDebugView.class.getSimpleName();
 
@@ -66,9 +75,11 @@
     private static final int KEY_VIEW_VERTICAL_PADDING_DP = 8;
     private static final int KEY_VIEW_MIN_WIDTH_DP = 32;
     private static final int KEY_VIEW_TEXT_SIZE_SP = 12;
+    private static final double ROTATY_GRAPH_HEIGHT_FRACTION = 0.5;
 
     private final InputManagerService mService;
     private final int mOuterPadding;
+    private final DisplayMetrics mDm;
 
     // Tracks all keys that are currently pressed/down.
     private final Map<Pair<Integer /*deviceId*/, Integer /*scanCode*/>, PressedKeyView>
@@ -80,18 +91,29 @@
     private PressedKeyContainer mPressedKeyContainer;
     @Nullable
     private PressedKeyContainer mPressedModifierContainer;
+    private final Supplier<RotaryInputValueView> mRotaryInputValueViewFactory;
+    @Nullable
+    private RotaryInputValueView mRotaryInputValueView;
+    private final Supplier<RotaryInputGraphView> mRotaryInputGraphViewFactory;
+    @Nullable
+    private RotaryInputGraphView mRotaryInputGraphView;
 
-    FocusEventDebugView(Context c, InputManagerService service) {
+    @VisibleForTesting
+    FocusEventDebugView(Context c, InputManagerService service,
+            Supplier<RotaryInputValueView> rotaryInputValueViewFactory,
+            Supplier<RotaryInputGraphView> rotaryInputGraphViewFactory) {
         super(c);
         setFocusableInTouchMode(true);
 
         mService = service;
-        final var dm = mContext.getResources().getDisplayMetrics();
-        mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm);
+        mRotaryInputValueViewFactory = rotaryInputValueViewFactory;
+        mRotaryInputGraphViewFactory = rotaryInputGraphViewFactory;
+        mDm = mContext.getResources().getDisplayMetrics();
+        mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, mDm);
+    }
 
-        setOrientation(HORIZONTAL);
-        setLayoutDirection(LAYOUT_DIRECTION_RTL);
-        setGravity(Gravity.START | Gravity.BOTTOM);
+    FocusEventDebugView(Context c, InputManagerService service) {
+        this(c, service, () -> new RotaryInputValueView(c), () -> new RotaryInputGraphView(c));
     }
 
     @Override
@@ -100,13 +122,13 @@
 
         final RoundedCorner bottomLeft =
                 insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
-        if (bottomLeft != null) {
+        if (bottomLeft != null && !insets.isRound()) {
             paddingBottom = bottomLeft.getRadius();
         }
 
         final RoundedCorner bottomRight =
                 insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
-        if (bottomRight != null) {
+        if (bottomRight != null && !insets.isRound()) {
             paddingBottom = Math.max(paddingBottom, bottomRight.getRadius());
         }
 
@@ -151,7 +173,7 @@
         }
 
         mPressedKeyContainer = new PressedKeyContainer(mContext);
-        mPressedKeyContainer.setOrientation(HORIZONTAL);
+        mPressedKeyContainer.setOrientation(LinearLayout.HORIZONTAL);
         mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM);
         mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR);
         final var scroller = new HorizontalScrollView(mContext);
@@ -160,15 +182,23 @@
         scroller.addOnLayoutChangeListener(
                 (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT));
         scroller.setHorizontalFadingEdgeEnabled(true);
-        addView(scroller, new LayoutParams(0, WRAP_CONTENT, 1));
+        LayoutParams scrollerLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+        scrollerLayoutParams.addRule(ALIGN_PARENT_BOTTOM);
+        scrollerLayoutParams.addRule(ALIGN_PARENT_RIGHT);
+        addView(scroller, scrollerLayoutParams);
 
         mPressedModifierContainer = new PressedKeyContainer(mContext);
-        mPressedModifierContainer.setOrientation(VERTICAL);
+        mPressedModifierContainer.setOrientation(LinearLayout.VERTICAL);
         mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM);
-        addView(mPressedModifierContainer, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
+        LayoutParams modifierLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+        modifierLayoutParams.addRule(ALIGN_PARENT_BOTTOM);
+        modifierLayoutParams.addRule(ALIGN_PARENT_LEFT);
+        modifierLayoutParams.addRule(LEFT_OF, scroller.getId());
+        addView(mPressedModifierContainer, modifierLayoutParams);
     }
 
-    private void handleUpdateShowRotaryInput(boolean enabled) {
+    @VisibleForTesting
+    void handleUpdateShowRotaryInput(boolean enabled) {
         if (enabled == showRotaryInput()) {
             return;
         }
@@ -176,10 +206,26 @@
         if (!enabled) {
             mFocusEventDebugGlobalMonitor.dispose();
             mFocusEventDebugGlobalMonitor = null;
+            removeView(mRotaryInputValueView);
+            mRotaryInputValueView = null;
+            removeView(mRotaryInputGraphView);
+            mRotaryInputGraphView = null;
             return;
         }
 
         mFocusEventDebugGlobalMonitor = new FocusEventDebugGlobalMonitor(this, mService);
+
+        mRotaryInputValueView = mRotaryInputValueViewFactory.get();
+        LayoutParams valueLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+        valueLayoutParams.addRule(CENTER_HORIZONTAL);
+        valueLayoutParams.addRule(ALIGN_PARENT_BOTTOM);
+        addView(mRotaryInputValueView, valueLayoutParams);
+
+        mRotaryInputGraphView = mRotaryInputGraphViewFactory.get();
+        LayoutParams graphLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+                (int) (ROTATY_GRAPH_HEIGHT_FRACTION * mDm.heightPixels));
+        graphLayoutParams.addRule(CENTER_IN_PARENT);
+        addView(mRotaryInputGraphView, graphLayoutParams);
     }
 
     /** Report a key event to the debug view. */
@@ -242,14 +288,15 @@
         keyEvent.recycle();
     }
 
-    private void handleRotaryInput(MotionEvent motionEvent) {
+    @VisibleForTesting
+    void handleRotaryInput(MotionEvent motionEvent) {
         if (!showRotaryInput()) {
             return;
         }
 
         float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL);
-        // TODO(b/286086154): replace log with visualization.
-        Log.d(TAG, "ROTARY INPUT: " + String.valueOf(scrollAxisValue));
+        mRotaryInputValueView.updateValue(scrollAxisValue);
+        mRotaryInputGraphView.addValue(scrollAxisValue, motionEvent.getEventTime());
 
         motionEvent.recycle();
     }
@@ -308,7 +355,14 @@
 
     /** Determine whether to show rotary input by checking one of the rotary-related objects. */
     private boolean showRotaryInput() {
-        return mFocusEventDebugGlobalMonitor != null;
+        return mRotaryInputValueView != null;
+    }
+
+    /**
+     * Converts a dimension in scaled pixel units to integer display pixels.
+     */
+    private static int applyDimensionSp(int dimensionSp, DisplayMetrics dm) {
+        return (int) TypedValue.applyDimension(COMPLEX_UNIT_SP, dimensionSp, dm);
     }
 
     private static class PressedKeyView extends TextView {
@@ -419,4 +473,376 @@
             invalidate();
         }
     }
+
+    // TODO(b/286086154): move RotaryInputGraphView and RotaryInputValueView to a subpackage.
+
+    /** Draws the most recent rotary input value and indicates whether the source is active. */
+    @VisibleForTesting
+    static class RotaryInputValueView extends TextView {
+
+        private static final int INACTIVE_TEXT_COLOR = 0xffff00ff;
+        private static final int ACTIVE_TEXT_COLOR = 0xff420f28;
+        private static final int TEXT_SIZE_SP = 8;
+        private static final int SIDE_PADDING_SP = 4;
+        /** Determines how long the active status lasts. */
+        private static final int ACTIVE_STATUS_DURATION = 250 /* milliseconds */;
+        private static final ColorFilter ACTIVE_BACKGROUND_FILTER =
+                new ColorMatrixColorFilter(new float[]{
+                        0, 0, 0, 0, 255, // red
+                        0, 0, 0, 0,   0, // green
+                        0, 0, 0, 0, 255, // blue
+                        0, 0, 0, 0, 200  // alpha
+                });
+
+        private final Runnable mUpdateActivityStatusCallback = () -> updateActivityStatus(false);
+        private final float mScaledVerticalScrollFactor;
+
+        @VisibleForTesting
+        RotaryInputValueView(Context c) {
+            super(c);
+
+            DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
+            mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor();
+
+            setText(getFormattedValue(0));
+            setTextColor(INACTIVE_TEXT_COLOR);
+            setTextSize(applyDimensionSp(TEXT_SIZE_SP, dm));
+            setPaddingRelative(applyDimensionSp(SIDE_PADDING_SP, dm), 0,
+                    applyDimensionSp(SIDE_PADDING_SP, dm), 0);
+            setTypeface(null, Typeface.BOLD);
+            setBackgroundResource(R.drawable.focus_event_rotary_input_background);
+        }
+
+        void updateValue(float value) {
+            removeCallbacks(mUpdateActivityStatusCallback);
+
+            setText(getFormattedValue(value * mScaledVerticalScrollFactor));
+
+            updateActivityStatus(true);
+            postDelayed(mUpdateActivityStatusCallback, ACTIVE_STATUS_DURATION);
+        }
+
+        @VisibleForTesting
+        void updateActivityStatus(boolean active) {
+            if (active) {
+                setTextColor(ACTIVE_TEXT_COLOR);
+                getBackground().setColorFilter(ACTIVE_BACKGROUND_FILTER);
+            } else {
+                setTextColor(INACTIVE_TEXT_COLOR);
+                getBackground().clearColorFilter();
+            }
+        }
+
+        private static String getFormattedValue(float value) {
+            return String.format("%s%.1f", value < 0 ? "-" : "+", Math.abs(value));
+        }
+    }
+
+    /**
+     * Shows a graph with the rotary input values as a function of time.
+     * The graph gets reset if no action is received for a certain amount of time.
+     */
+    @VisibleForTesting
+    static class RotaryInputGraphView extends View {
+
+        private static final int FRAME_COLOR = 0xbf741b47;
+        private static final int FRAME_WIDTH_SP = 2;
+        private static final int FRAME_BORDER_GAP_SP = 10;
+        private static final int FRAME_TEXT_SIZE_SP = 10;
+        private static final int FRAME_TEXT_OFFSET_SP = 2;
+        private static final int GRAPH_COLOR = 0xffff00ff;
+        private static final int GRAPH_LINE_WIDTH_SP = 1;
+        private static final int GRAPH_POINT_RADIUS_SP = 4;
+        private static final long MAX_SHOWN_TIME_INTERVAL = TimeUnit.SECONDS.toMillis(5);
+        private static final float DEFAULT_FRAME_CENTER_POSITION = 0;
+        private static final int MAX_GRAPH_VALUES_SIZE = 400;
+        /** Maximum time between values so that they are considered part of the same gesture. */
+        private static final long MAX_GESTURE_TIME = TimeUnit.SECONDS.toMillis(1);
+
+        private final DisplayMetrics mDm;
+        /**
+         * Distance in position units (amount scrolled in display pixels) from the center to the
+         * top/bottom frame lines.
+         */
+        private final float mFrameCenterToBorderDistance;
+        private final float mScaledVerticalScrollFactor;
+        private final Locale mDefaultLocale;
+        private final Paint mFramePaint = new Paint();
+        private final Paint mFrameTextPaint = new Paint();
+        private final Paint mGraphLinePaint = new Paint();
+        private final Paint mGraphPointPaint = new Paint();
+
+        private final CyclicBuffer mGraphValues = new CyclicBuffer(MAX_GRAPH_VALUES_SIZE);
+        /** Position at which graph values are placed at the center of the graph. */
+        private float mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION;
+
+        @VisibleForTesting
+        RotaryInputGraphView(Context c) {
+            super(c);
+
+            mDm = mContext.getResources().getDisplayMetrics();
+            // This makes the center-to-border distance equivalent to the display height, meaning
+            // that the total height of the graph is equivalent to 2x the display height.
+            mFrameCenterToBorderDistance = mDm.heightPixels;
+            mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor();
+            mDefaultLocale = Locale.getDefault();
+
+            mFramePaint.setColor(FRAME_COLOR);
+            mFramePaint.setStrokeWidth(applyDimensionSp(FRAME_WIDTH_SP, mDm));
+
+            mFrameTextPaint.setColor(GRAPH_COLOR);
+            mFrameTextPaint.setTextSize(applyDimensionSp(FRAME_TEXT_SIZE_SP, mDm));
+
+            mGraphLinePaint.setColor(GRAPH_COLOR);
+            mGraphLinePaint.setStrokeWidth(applyDimensionSp(GRAPH_LINE_WIDTH_SP, mDm));
+            mGraphLinePaint.setStrokeCap(Paint.Cap.ROUND);
+            mGraphLinePaint.setStrokeJoin(Paint.Join.ROUND);
+
+            mGraphPointPaint.setColor(GRAPH_COLOR);
+            mGraphPointPaint.setStrokeWidth(applyDimensionSp(GRAPH_POINT_RADIUS_SP, mDm));
+            mGraphPointPaint.setStrokeCap(Paint.Cap.ROUND);
+            mGraphPointPaint.setStrokeJoin(Paint.Join.ROUND);
+        }
+
+        /**
+         * Reads new scroll axis value and updates the list accordingly. Old positions are
+         * kept at the front (what you would get with getFirst), while the recent positions are
+         * kept at the back (what you would get with getLast). Also updates the frame center
+         * position to handle out-of-bounds cases.
+         */
+        void addValue(float scrollAxisValue, long eventTime) {
+            // Remove values that are too old.
+            while (mGraphValues.getSize() > 0
+                    && (eventTime - mGraphValues.getFirst().mTime) > MAX_SHOWN_TIME_INTERVAL) {
+                mGraphValues.removeFirst();
+            }
+
+            // If there are no recent values, reset the frame center.
+            if (mGraphValues.getSize() == 0) {
+                mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION;
+            }
+
+            // Handle new value. We multiply the scroll axis value by the scaled scroll factor to
+            // get the amount of pixels to be scrolled. We also compute the accumulated position
+            // by adding the current value to the last one (if not empty).
+            final float displacement = scrollAxisValue * mScaledVerticalScrollFactor;
+            final float prevPos = (mGraphValues.getSize() == 0 ? 0 : mGraphValues.getLast().mPos);
+            final float pos = prevPos + displacement;
+
+            mGraphValues.add(pos, eventTime);
+
+            // The difference between the distance of the most recent position from the center
+            // frame (pos - mFrameCenterPosition) and the maximum allowed distance from the center
+            // frame (mFrameCenterToBorderDistance).
+            final float verticalDiff = Math.abs(pos - mFrameCenterPosition)
+                    - mFrameCenterToBorderDistance;
+            // If needed, translate frame.
+            if (verticalDiff > 0) {
+                final int sign = pos - mFrameCenterPosition < 0 ? -1 : 1;
+                // Here, we update the center frame position by the exact amount needed for us to
+                // stay within the maximum allowed distance from the center frame.
+                mFrameCenterPosition += sign * verticalDiff;
+            }
+
+            // Redraw canvas.
+            invalidate();
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            super.onDraw(canvas);
+
+            // Note: vertical coordinates in Canvas go from top to bottom,
+            // that is bottomY > middleY > topY.
+            final int verticalMargin = applyDimensionSp(FRAME_BORDER_GAP_SP, mDm);
+            final int topY = verticalMargin;
+            final int bottomY = getHeight() - verticalMargin;
+            final int middleY = (topY + bottomY) / 2;
+
+            // Note: horizontal coordinates in Canvas go from left to right,
+            // that is rightX > leftX.
+            final int leftX = 0;
+            final int rightX = getWidth();
+
+            // Draw the frame, which includes 3 lines that show the maximum,
+            // minimum and middle positions of the graph.
+            canvas.drawLine(leftX, topY, rightX, topY, mFramePaint);
+            canvas.drawLine(leftX, middleY, rightX, middleY, mFramePaint);
+            canvas.drawLine(leftX, bottomY, rightX, bottomY, mFramePaint);
+
+            // Draw the position that each frame line corresponds to.
+            final int frameTextOffset = applyDimensionSp(FRAME_TEXT_OFFSET_SP, mDm);
+            canvas.drawText(
+                    String.format(mDefaultLocale, "%.1f",
+                            mFrameCenterPosition + mFrameCenterToBorderDistance),
+                    leftX,
+                    topY - frameTextOffset, mFrameTextPaint
+            );
+            canvas.drawText(
+                    String.format(mDefaultLocale, "%.1f", mFrameCenterPosition),
+                    leftX,
+                    middleY - frameTextOffset, mFrameTextPaint
+            );
+            canvas.drawText(
+                    String.format(mDefaultLocale, "%.1f",
+                            mFrameCenterPosition - mFrameCenterToBorderDistance),
+                    leftX,
+                    bottomY - frameTextOffset, mFrameTextPaint
+            );
+
+            // If there are no graph values to be drawn, stop here.
+            if (mGraphValues.getSize() == 0) {
+                return;
+            }
+
+            // Draw the graph using the times and positions.
+            // We start at the most recent value (which should be drawn at the right) and move
+            // to the older values (which should be drawn to the left of more recent ones). Negative
+            // indices are handled by circuling back to the end of the buffer.
+            final long mostRecentTime = mGraphValues.getLast().mTime;
+            float prevCoordX = 0;
+            float prevCoordY = 0;
+            float prevAge = 0;
+            for (Iterator<GraphValue> iter = mGraphValues.reverseIterator(); iter.hasNext();) {
+                final GraphValue value = iter.next();
+
+                final int age = (int) (mostRecentTime - value.mTime);
+                final float pos = value.mPos;
+
+                // We get the horizontal coordinate in time units from left to right with
+                // (MAX_SHOWN_TIME_INTERVAL - age). Then, we rescale it to match the canvas
+                // units by dividing it by the time-domain length (MAX_SHOWN_TIME_INTERVAL)
+                // and by multiplying it by the canvas length (rightX - leftX). Finally, we
+                // offset the coordinate by adding it to leftX.
+                final float coordX = leftX + ((float) (MAX_SHOWN_TIME_INTERVAL - age)
+                        / MAX_SHOWN_TIME_INTERVAL) * (rightX - leftX);
+
+                // We get the vertical coordinate in position units from middle to top with
+                // (pos - mFrameCenterPosition). Then, we rescale it to match the canvas
+                // units by dividing it by half of the position-domain length
+                // (mFrameCenterToBorderDistance) and by multiplying it by half of the canvas
+                // length (middleY - topY). Finally, we offset the coordinate by subtracting
+                // it from middleY (we can't "add" here because the coordinate grows from top
+                // to bottom).
+                final float coordY = middleY - ((pos - mFrameCenterPosition)
+                        / mFrameCenterToBorderDistance) * (middleY - topY);
+
+                // Draw a point for this value.
+                canvas.drawPoint(coordX, coordY, mGraphPointPaint);
+
+                // If this value is part of the same gesture as the previous one, draw a line
+                // between them. We ignore the first value (with age = 0).
+                if (age != 0 && (age - prevAge) <= MAX_GESTURE_TIME) {
+                    canvas.drawLine(prevCoordX, prevCoordY, coordX, coordY, mGraphLinePaint);
+                }
+
+                prevCoordX = coordX;
+                prevCoordY = coordY;
+                prevAge = age;
+            }
+        }
+
+        @VisibleForTesting
+        float getFrameCenterPosition() {
+            return mFrameCenterPosition;
+        }
+
+        /**
+         * Holds data needed to draw each entry in the graph.
+         */
+        private static class GraphValue {
+            /** Position. */
+            float mPos;
+            /** Time when this value was added. */
+            long mTime;
+
+            GraphValue(float pos, long time) {
+                this.mPos = pos;
+                this.mTime = time;
+            }
+        }
+
+        /**
+         * Holds the graph values as a cyclic buffer. It has a fixed capacity, and it replaces the
+         * old values with new ones to avoid creating new objects.
+         */
+        private static class CyclicBuffer {
+            private final GraphValue[] mValues;
+            private final int mCapacity;
+            private int mSize = 0;
+            private int mLastIndex = 0;
+
+            // The iteration index and counter are here to make it easier to reset them.
+            /** Determines the value currently pointed by the iterator. */
+            private int mIteratorIndex;
+            /** Counts how many values have been iterated through. */
+            private int mIteratorCount;
+
+            /** Used traverse the values in reverse order. */
+            private final Iterator<GraphValue> mReverseIterator = new Iterator<GraphValue>() {
+                @Override
+                public boolean hasNext() {
+                    return mIteratorCount <= mSize;
+                }
+
+                @Override
+                public GraphValue next() {
+                    // Returns the value currently pointed by the iterator and moves the iterator to
+                    // the previous one.
+                    mIteratorCount++;
+                    return mValues[(mIteratorIndex-- + mCapacity) % mCapacity];
+                }
+            };
+
+            CyclicBuffer(int capacity) {
+                mCapacity = capacity;
+                mValues = new GraphValue[capacity];
+            }
+
+            /**
+             * Add new graph value. If there is an existing object, we replace its data with the
+             * new one. With this, we re-use old objects instead of creating new ones.
+             */
+            void add(float pos, long time) {
+                mLastIndex = (mLastIndex + 1) % mCapacity;
+                if (mValues[mLastIndex] == null) {
+                    mValues[mLastIndex] = new GraphValue(pos, time);
+                } else {
+                    final GraphValue oldValue = mValues[mLastIndex];
+                    oldValue.mPos = pos;
+                    oldValue.mTime = time;
+                }
+
+                // If needed, account for new value in the buffer size.
+                if (mSize != mCapacity) {
+                    mSize++;
+                }
+            }
+
+            int getSize() {
+                return mSize;
+            }
+
+            GraphValue getFirst() {
+                final int distanceBetweenLastAndFirst = (mCapacity - mSize) + 1;
+                final int firstIndex = (mLastIndex + distanceBetweenLastAndFirst) % mCapacity;
+                return mValues[firstIndex];
+            }
+
+            GraphValue getLast() {
+                return mValues[mLastIndex];
+            }
+
+            void removeFirst() {
+                mSize--;
+            }
+
+            /** Returns an iterator pointing at the last value. */
+            Iterator<GraphValue> reverseIterator() {
+                mIteratorIndex = mLastIndex;
+                mIteratorCount = 1;
+                return mReverseIterator;
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e782ea9..009cc3b 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1239,7 +1239,9 @@
                 mNotificationRecordLogger.log(
                         NotificationRecordLogger.NotificationEvent.fromAction(actionIndex,
                                 generatedByAssistant, action.isContextual()), r);
-                EventLogTags.writeNotificationActionClicked(key, actionIndex,
+                EventLogTags.writeNotificationActionClicked(key,
+                        action.actionIntent.getTarget().toString(),
+                        action.actionIntent.getIntent().toString(), actionIndex,
                         r.getLifespanMs(now), r.getFreshnessMs(now), r.getExposureMs(now),
                         nv.rank, nv.count);
                 nv.recycle();
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index 6e75605..f987629 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -487,6 +487,8 @@
 
     boolean isPackageSuspendedForUser(@NonNull String packageName, @UserIdInt int userId);
 
+    boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId);
+
     boolean isSuspendingAnyPackages(@NonNull String suspendingPackage, @UserIdInt int userId);
 
     @NonNull
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 7d878ec..1cfc7d7 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -4925,8 +4925,8 @@
         }
     }
 
-    @Override
-    public boolean isPackageSuspendedForUser(@NonNull String packageName, int userId) {
+    private PackageUserStateInternal getUserStageOrDefaultForUser(@NonNull String packageName,
+            int userId) {
         final int callingUid = Binder.getCallingUid();
         enforceCrossUserPermission(callingUid, userId, true /* requireFullPermission */,
                 false /* checkShell */, "isPackageSuspendedForUser for user " + userId);
@@ -4934,7 +4934,17 @@
         if (ps == null || shouldFilterApplicationIncludingUninstalled(ps, callingUid, userId)) {
             throw new IllegalArgumentException("Unknown target package: " + packageName);
         }
-        return ps.getUserStateOrDefault(userId).isSuspended();
+        return ps.getUserStateOrDefault(userId);
+    }
+
+    @Override
+    public boolean isPackageSuspendedForUser(@NonNull String packageName, int userId) {
+        return getUserStageOrDefaultForUser(packageName, userId).isSuspended();
+    }
+
+    @Override
+    public boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId) {
+        return getUserStageOrDefaultForUser(packageName, userId).isQuarantined();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java
index fd47846..76203ac 100644
--- a/services/core/java/com/android/server/pm/IPackageManagerBase.java
+++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java
@@ -955,6 +955,13 @@
 
     @Override
     @Deprecated
+    public final boolean isPackageQuarantinedForUser(@NonNull String packageName,
+            @UserIdInt int userId) {
+        return snapshot().isPackageQuarantinedForUser(packageName, userId);
+    }
+
+    @Override
+    @Deprecated
     public final boolean isSafeMode() {
         // allow instant applications
         return mService.getSafeMode();
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 303f321..6270655 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -68,6 +68,7 @@
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
 import android.content.ComponentName;
 import android.content.Context;
@@ -312,6 +313,19 @@
     private static final long SILENT_INSTALL_ALLOWED = 265131695L;
 
     /**
+     * The system supports pre-approval and update ownership features from
+     * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE API 34}. The change id is used to make sure
+     * the system includes the fix of pre-approval with update ownership case. When checking the
+     * change id, if it is disabled, it means the build includes the fix. The more detail is on
+     * b/293644536.
+     * See {@link PackageInstaller.SessionParams#setRequestUpdateOwnership(boolean)} and
+     * {@link #requestUserPreapproval(PreapprovalDetails, IntentSender)} for more details.
+     */
+    @Disabled
+    @ChangeId
+    private static final long PRE_APPROVAL_WITH_UPDATE_OWNERSHIP_FIX = 293644536L;
+
+    /**
      * The default value of {@link #mValidatedTargetSdk} is {@link Integer#MAX_VALUE}. If {@link
      * #mValidatedTargetSdk} is compared with {@link Build.VERSION_CODES#S} before getting the
      * target sdk version from a validated apk in {@link #validateApkInstallLocked()}, the compared
@@ -927,16 +941,27 @@
             if (mPermissionsManuallyAccepted) {
                 return USER_ACTION_NOT_NEEDED;
             }
-            packageName = mPackageName;
+            // For pre-pappvoal case, the mPackageName would be null.
+            if (mPackageName != null) {
+                packageName = mPackageName;
+            } else if (mPreapprovalRequested.get() && mPreapprovalDetails != null) {
+                packageName = mPreapprovalDetails.getPackageName();
+            } else {
+                packageName = null;
+            }
             hasDeviceAdminReceiver = mHasDeviceAdminReceiver;
         }
 
-        final boolean forcePermissionPrompt =
+        // For the below cases, force user action prompt
+        // 1. installFlags includes INSTALL_FORCE_PERMISSION_PROMPT
+        // 2. params.requireUserAction is USER_ACTION_REQUIRED
+        final boolean forceUserActionPrompt =
                 (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0
                         || params.requireUserAction == SessionParams.USER_ACTION_REQUIRED;
-        if (forcePermissionPrompt) {
-            return USER_ACTION_REQUIRED;
-        }
+        final int userActionNotTypicallyNeededResponse = forceUserActionPrompt
+                ? USER_ACTION_REQUIRED
+                : USER_ACTION_NOT_NEEDED;
+
         // It is safe to access mInstallerUid and mInstallSource without lock
         // because they are immutable after sealing.
         final Computer snapshot = mPm.snapshotComputer();
@@ -990,7 +1015,7 @@
                 || isInstallerDeviceOwnerOrAffiliatedProfileOwner();
 
         if (noUserActionNecessary) {
-            return USER_ACTION_NOT_NEEDED;
+            return userActionNotTypicallyNeededResponse;
         }
 
         if (isUpdateOwnershipEnforcementEnabled
@@ -1003,7 +1028,7 @@
         }
 
         if (isPermissionGranted) {
-            return USER_ACTION_NOT_NEEDED;
+            return userActionNotTypicallyNeededResponse;
         }
 
         if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid,
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index 6efd067..651845e 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -755,9 +755,7 @@
     @Override
     public boolean isPackageQuarantined(@NonNull String packageName,
             @UserIdInt int userId) {
-        final PackageStateInternal packageState = getPackageStateInternal(packageName);
-        return (packageState == null) ? false
-                : packageState.getUserStateOrDefault(userId).isQuarantined();
+        return snapshot().isPackageQuarantinedForUser(packageName, userId);
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index ceae1fe..8bdbe04 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -226,6 +226,8 @@
                     return runPath();
                 case "dump":
                     return runDump();
+                case "dump-package":
+                    return runDumpPackage();
                 case "list":
                     return runList();
                 case "gc":
@@ -978,6 +980,7 @@
         boolean listInstaller = false;
         boolean showUid = false;
         boolean showVersionCode = false;
+        boolean listQuarantinedOnly = false;
         boolean listApexOnly = false;
         boolean showStopped = false;
         int uid = -1;
@@ -1008,6 +1011,9 @@
                     case "-s":
                         listSystem = true;
                         break;
+                    case "-q":
+                        listQuarantinedOnly = true;
+                        break;
                     case "-U":
                         showUid = true;
                         break;
@@ -1093,6 +1099,10 @@
                         || (listApexOnly && !isApex)) {
                     continue;
                 }
+                if (listQuarantinedOnly && !mInterface.isPackageQuarantinedForUser(info.packageName,
+                        translatedUserId)) {
+                    continue;
+                }
 
                 String name = null;
                 if (showSdks) {
@@ -3598,6 +3608,23 @@
         return 0;
     }
 
+    private int runDumpPackage() {
+        String pkg = getNextArg();
+        if (pkg == null) {
+            getErrPrintWriter().println("Error: no package specified");
+            return 1;
+        }
+        try {
+            ((IBinder) mInterface).dump(getOutFileDescriptor(), new String[]{pkg});
+        } catch (Throwable e) {
+            PrintWriter pw = getErrPrintWriter();
+            pw.println("Failure dumping service:");
+            e.printStackTrace(pw);
+            pw.flush();
+        }
+        return 0;
+    }
+
     private int runSetHarmfulAppWarning() throws RemoteException {
         int userId = UserHandle.USER_CURRENT;
 
@@ -4282,6 +4309,9 @@
         pw.println("  dump PACKAGE");
         pw.println("    Print various system state associated with the given PACKAGE.");
         pw.println("");
+        pw.println("  dump-package PACKAGE");
+        pw.println("    Print package manager state associated with the given PACKAGE.");
+        pw.println("");
         pw.println("  has-feature FEATURE_NAME [version]");
         pw.println("    Prints true and returns exit status 0 when system has a FEATURE_NAME,");
         pw.println("    otherwise prints false and returns exit status 1");
@@ -4299,7 +4329,7 @@
         pw.println("    Options:");
         pw.println("      -v: shows the location of the library in the device's filesystem");
         pw.println("");
-        pw.println("  list packages [-f] [-d] [-e] [-s] [-3] [-i] [-l] [-u] [-U] ");
+        pw.println("  list packages [-f] [-d] [-e] [-s] [-q] [-3] [-i] [-l] [-u] [-U] ");
         pw.println("      [--show-versioncode] [--apex-only] [--factory-only]");
         pw.println("      [--uid UID] [--user USER_ID] [FILTER]");
         pw.println("    Prints all packages; optionally only those whose name contains");
@@ -4309,6 +4339,7 @@
         pw.println("      -d: filter to only show disabled packages");
         pw.println("      -e: filter to only show enabled packages");
         pw.println("      -s: filter to only show system packages");
+        pw.println("      -q: filter to only show quarantined packages");
         pw.println("      -3: filter to only show third party packages");
         pw.println("      -i: see the installer for the packages");
         pw.println("      -l: ignored (used for compatibility with older releases)");
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index faf132e..2f68021 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -208,7 +208,6 @@
 import com.android.internal.policy.PhoneWindow;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.AccessibilityManagerInternal;
 import com.android.server.ExtconStateObserver;
@@ -622,9 +621,16 @@
     private boolean mAllowTheaterModeWakeFromLidSwitch;
     private boolean mAllowTheaterModeWakeFromWakeGesture;
 
-    // Whether to support long press from power button in non-interactive mode
+    // If true, the power button long press behavior will be invoked even if the default display is
+    // non-interactive. If false, the power button long press behavior will be skipped if the
+    // default display is non-interactive.
     private boolean mSupportLongPressPowerWhenNonInteractive;
 
+    // If true, the power button short press behavior will be always invoked as long as the default
+    // display is on, even if the display is not interactive. If false, the power button short press
+    // behavior will be skipped if the default display is non-interactive.
+    private boolean mSupportShortPressPowerWhenDefaultDisplayOn;
+
     // Whether to go to sleep entering theater mode from power button
     private boolean mGoToSleepOnButtonPressTheaterMode;
 
@@ -1041,7 +1047,7 @@
         }
     }
 
-    private void powerPress(long eventTime, int count, boolean beganFromNonInteractive) {
+    private void powerPress(long eventTime, int count) {
         // SideFPS still needs to know about suppressed power buttons, in case it needs to block
         // an auth attempt.
         if (count == 1) {
@@ -1055,9 +1061,16 @@
 
         final boolean interactive = mDefaultDisplayPolicy.isAwake();
 
-        Slog.d(TAG, "powerPress: eventTime=" + eventTime + " interactive=" + interactive
-                + " count=" + count + " beganFromNonInteractive=" + beganFromNonInteractive
-                + " mShortPressOnPowerBehavior=" + mShortPressOnPowerBehavior);
+        Slog.d(
+                TAG,
+                "powerPress: eventTime="
+                        + eventTime
+                        + " interactive="
+                        + interactive
+                        + " count="
+                        + count
+                        + " mShortPressOnPowerBehavior="
+                        + mShortPressOnPowerBehavior);
 
         if (count == 2) {
             powerMultiPressAction(eventTime, interactive, mDoublePressOnPowerBehavior);
@@ -1065,12 +1078,7 @@
             powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior);
         } else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
             Slog.d(TAG, "No behavior defined for power press count " + count);
-        } else if (count == 1 && interactive && !beganFromNonInteractive) {
-            if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) {
-                Slog.i(TAG, "Suppressing power key because the user is interacting with the "
-                        + "fingerprint sensor");
-                return;
-            }
+        } else if (count == 1 && shouldHandleShortPressPowerAction(interactive, eventTime)) {
             switch (mShortPressOnPowerBehavior) {
                 case SHORT_PRESS_POWER_NOTHING:
                     break;
@@ -1118,6 +1126,44 @@
         }
     }
 
+    private boolean shouldHandleShortPressPowerAction(boolean interactive, long eventTime) {
+        if (mSupportShortPressPowerWhenDefaultDisplayOn) {
+            final boolean defaultDisplayOn = Display.isOnState(mDefaultDisplay.getState());
+            final boolean beganFromDefaultDisplayOn =
+                    mSingleKeyGestureDetector.beganFromDefaultDisplayOn();
+            if (!defaultDisplayOn || !beganFromDefaultDisplayOn) {
+                Slog.v(
+                        TAG,
+                        "Ignoring short press of power button because the default display is not"
+                                + " on. defaultDisplayOn="
+                                + defaultDisplayOn
+                                + ", beganFromDefaultDisplayOn="
+                                + beganFromDefaultDisplayOn);
+                return false;
+            }
+            return true;
+        }
+        final boolean beganFromNonInteractive = mSingleKeyGestureDetector.beganFromNonInteractive();
+        if (!interactive || beganFromNonInteractive) {
+            Slog.v(
+                    TAG,
+                    "Ignoring short press of power button because the device is not interactive."
+                            + " interactive="
+                            + interactive
+                            + ", beganFromNonInteractive="
+                            + beganFromNonInteractive);
+            return false;
+        }
+        if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) {
+            Slog.i(
+                    TAG,
+                    "Suppressing power key because the user is interacting with the "
+                            + "fingerprint sensor");
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Attempt to dream from a power button press.
      *
@@ -2231,6 +2277,11 @@
 
         mSupportLongPressPowerWhenNonInteractive = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_supportLongPressPowerWhenNonInteractive);
+        mSupportShortPressPowerWhenDefaultDisplayOn =
+                mContext.getResources()
+                        .getBoolean(
+                                com.android.internal.R.bool
+                                        .config_supportShortPressPowerWhenDefaultDisplayOn);
 
         mLongPressOnBackBehavior = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_longPressOnBackBehavior);
@@ -2518,8 +2569,7 @@
 
         @Override
         void onPress(long downTime) {
-            powerPress(downTime, 1 /*count*/,
-                    mSingleKeyGestureDetector.beganFromNonInteractive());
+            powerPress(downTime, 1 /*count*/);
         }
 
         @Override
@@ -2550,7 +2600,7 @@
 
         @Override
         void onMultiPress(long downTime, int count) {
-            powerPress(downTime, count, mSingleKeyGestureDetector.beganFromNonInteractive());
+            powerPress(downTime, count);
         }
     }
 
@@ -4323,10 +4373,11 @@
 
         // This could prevent some wrong state in multi-displays environment,
         // the default display may turned off but interactive is true.
-        final boolean isDefaultDisplayOn = mDefaultDisplayPolicy.isAwake();
-        final boolean interactiveAndOn = interactive && isDefaultDisplayOn;
+        final boolean isDefaultDisplayOn = Display.isOnState(mDefaultDisplay.getState());
+        final boolean isDefaultDisplayAwake = mDefaultDisplayPolicy.isAwake();
+        final boolean interactiveAndAwake = interactive && isDefaultDisplayAwake;
         if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
-            handleKeyGesture(event, interactiveAndOn);
+            handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn);
         }
 
         // Enable haptics if down and virtual key without multiple repetitions. If this is a hard
@@ -4479,7 +4530,7 @@
                 result &= ~ACTION_PASS_TO_USER;
                 isWakeKey = false; // wake-up will be handled separately
                 if (down) {
-                    interceptPowerKeyDown(event, interactiveAndOn);
+                    interceptPowerKeyDown(event, interactiveAndAwake);
                 } else {
                     interceptPowerKeyUp(event, canceled);
                 }
@@ -4695,7 +4746,7 @@
         return result;
     }
 
-    private void handleKeyGesture(KeyEvent event, boolean interactive) {
+    private void handleKeyGesture(KeyEvent event, boolean interactive, boolean defaultDisplayOn) {
         if (mKeyCombinationManager.interceptKey(event, interactive)) {
             // handled by combo keys manager.
             mSingleKeyGestureDetector.reset();
@@ -4711,7 +4762,7 @@
             }
         }
 
-        mSingleKeyGestureDetector.interceptKey(event, interactive);
+        mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn);
     }
 
     // The camera gesture will be detected by GestureLauncherService.
@@ -6167,6 +6218,9 @@
                 pw.print("mTriplePressOnPowerBehavior=");
                 pw.println(multiPressOnPowerBehaviorToString(mTriplePressOnPowerBehavior));
         pw.print(prefix);
+                pw.print("mSupportShortPressPowerWhenDefaultDisplayOn=");
+                pw.println(mSupportShortPressPowerWhenDefaultDisplayOn);
+        pw.print(prefix);
         pw.print("mPowerVolUpBehavior=");
         pw.println(powerVolumeUpBehaviorToString(mPowerVolUpBehavior));
         pw.print(prefix);
diff --git a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
index b999bbb3..5fc0637 100644
--- a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
+++ b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
@@ -43,6 +43,7 @@
 
     private int mKeyPressCounter;
     private boolean mBeganFromNonInteractive = false;
+    private boolean mBeganFromDefaultDisplayOn = false;
 
     private final ArrayList<SingleKeyRule> mRules = new ArrayList();
     private SingleKeyRule mActiveRule = null;
@@ -194,11 +195,12 @@
         mRules.remove(rule);
     }
 
-    void interceptKey(KeyEvent event, boolean interactive) {
+    void interceptKey(KeyEvent event, boolean interactive, boolean defaultDisplayOn) {
         if (event.getAction() == KeyEvent.ACTION_DOWN) {
-            // Store the non interactive state when first down.
+            // Store the non interactive state and display on state when first down.
             if (mDownKeyCode == KeyEvent.KEYCODE_UNKNOWN || mDownKeyCode != event.getKeyCode()) {
                 mBeganFromNonInteractive = !interactive;
+                mBeganFromDefaultDisplayOn = defaultDisplayOn;
             }
             interceptKeyDown(event);
         } else {
@@ -388,6 +390,10 @@
         return mBeganFromNonInteractive;
     }
 
+    boolean beganFromDefaultDisplayOn() {
+        return mBeganFromDefaultDisplayOn;
+    }
+
     void dump(String prefix, PrintWriter pw) {
         pw.println(prefix + "SingleKey rules:");
         for (SingleKeyRule rule : mRules) {
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index cb09aef..5b36cd6 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -3055,10 +3055,10 @@
         TvInputInfo tvInputInfo = tvInputState.info;
         int inputState = tvInputState.state;
         int inputType = tvInputInfo.getType();
-        // For non-CEC input, the value of vendorId is 0.
-        int vendorId = 0;
-        // For non-HDMI input, the value of hdmiPort is 0.
-        int hdmiPort = 0;
+        // For non-CEC input, the value of vendorId is 0xFFFFFF (16777215 in decimal).
+        int vendorId = 16777215;
+        // For non-HDMI input, the value of hdmiPort is -1.
+        int hdmiPort = -1;
         String tifSessionId = sessionState.sessionId;
 
         if (tvInputInfo.getType() == TvInputInfo.TYPE_HDMI) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 6c848d1..fb62412 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -2606,9 +2606,6 @@
         final int callingUid = Binder.getCallingUid();
         final long ident = Binder.clearCallingIdentity();
         try {
-            // When a task is locked, dismiss the root pinned task if it exists
-            mRootWindowContainer.removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED);
-
             getLockTaskController().startLockTaskMode(task, isSystemCaller, callingUid);
         } finally {
             Binder.restoreCallingIdentity(ident);
diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java
index 0c98fb5..0f9998c 100644
--- a/services/core/java/com/android/server/wm/LockTaskController.java
+++ b/services/core/java/com/android/server/wm/LockTaskController.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.Context.DEVICE_POLICY_SERVICE;
 import static android.content.Context.STATUS_BAR_SERVICE;
@@ -669,6 +670,9 @@
             }
         }
 
+        // When a task is locked, dismiss the root pinned task if it exists 
+        mSupervisor.mRootWindowContainer.removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED);
+
         // System can only initiate screen pinning, not full lock task mode
         ProtoLog.w(WM_DEBUG_LOCKTASK, "%s", isSystemCaller ? "Locking pinned" : "Locking fully");
         setLockTaskMode(task, isSystemCaller ? LOCK_TASK_MODE_PINNED : LOCK_TASK_MODE_LOCKED,
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 6432ff0..6ede345 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -1124,14 +1124,15 @@
                         + "track #%d", transition.getSyncId(), track);
             }
         }
-        if (sync) {
+        transition.mAnimationTrack = track;
+        info.setTrack(track);
+        mTrackCount = Math.max(mTrackCount, track + 1);
+        if (sync && mTrackCount > 1) {
+            // If there are >1 tracks, mark as sync so that all tracks finish.
             info.setFlags(info.getFlags() | TransitionInfo.FLAG_SYNC);
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Marking #%d animation as SYNC.",
                     transition.getSyncId());
         }
-        transition.mAnimationTrack = track;
-        info.setTrack(track);
-        mTrackCount = Math.max(mTrackCount, track + 1);
     }
 
     void updateAnimatingState(SurfaceControl.Transaction t) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 5a45fe1..a172d99 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -30,7 +30,6 @@
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.SurfaceControl.Transaction;
 import static android.view.SurfaceControl.getGlobalTransaction;
-import static android.view.ViewRootImpl.LOCAL_LAYOUT;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
@@ -1446,9 +1445,7 @@
         }
 
         final boolean dragResizingChanged = !mDragResizingChangeReported && isDragResizeChanged();
-
-        final boolean attachedFrameChanged = LOCAL_LAYOUT
-                && mLayoutAttached && getParentWindow().frameChanged();
+        final boolean attachedFrameChanged = mLayoutAttached && getParentWindow().frameChanged();
 
         if (DEBUG) {
             Slog.v(TAG_WM, "Resizing " + this + ": configChanged=" + configChanged
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java
index 74dc853..7552800 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java
@@ -56,7 +56,7 @@
 import android.os.RemoteException;
 import android.os.StatFs;
 import android.os.SystemClock;
-import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.Postsubmit;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
@@ -93,7 +93,7 @@
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeUnit;
 
-@Presubmit
+@Postsubmit
 public class PackageManagerTests extends AndroidTestCase {
     private static final boolean localLOGV = true;
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java
new file mode 100644
index 0000000..4fd8f26
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 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.display;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.SurfaceControl;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the {@link DisplayDevice} class.
+ *
+ * Build/Install/Run:
+ * atest DisplayServicesTests:DisplayDeviceTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DisplayDeviceTest {
+    private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
+    private static final int WIDTH = 500;
+    private static final int HEIGHT = 900;
+    private static final Point PORTRAIT_SIZE = new Point(WIDTH, HEIGHT);
+    private static final Point LANDSCAPE_SIZE = new Point(HEIGHT, WIDTH);
+
+    @Mock
+    private SurfaceControl.Transaction mMockTransaction;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mDisplayDeviceInfo.width = WIDTH;
+        mDisplayDeviceInfo.height = HEIGHT;
+        mDisplayDeviceInfo.rotation = ROTATION_0;
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_notRotated() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation0() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_0, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation90() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_90, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation180() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_180, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation270() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_270, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE);
+    }
+
+    private static class FakeDisplayDevice extends DisplayDevice {
+        private final DisplayDeviceInfo mDisplayDeviceInfo;
+
+        FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo) {
+            super(null, null, "", InstrumentationRegistry.getInstrumentation().getContext());
+            mDisplayDeviceInfo = displayDeviceInfo;
+        }
+
+        @Override
+        public boolean hasStableUniqueId() {
+            return false;
+        }
+
+        @Override
+        public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+            return mDisplayDeviceInfo;
+        }
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
index de27d77..e672928 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
@@ -102,7 +102,7 @@
         ).when(() -> Settings.Global.getString(any(), anyString()));
 
         mTestMapper = new SettingsToPropertiesMapper(
-            mMockContentResolver, TEST_MAPPING, new String[] {});
+            mMockContentResolver, TEST_MAPPING, new String[] {}, new String[] {});
     }
 
     @After
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index bcbbcd4..908afc8 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -61,6 +61,7 @@
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorCallback;
 import android.companion.virtual.sensor.VirtualSensorConfig;
+import android.content.AttributionSource;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -1729,7 +1730,9 @@
     private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid,
             VirtualDeviceParams params) {
         VirtualDeviceImpl virtualDeviceImpl = new VirtualDeviceImpl(mContext,
-                mAssociationInfo, mVdms, new Binder(), ownerUid, virtualDeviceId,
+                mAssociationInfo, mVdms, new Binder(),
+                new AttributionSource(ownerUid, "com.android.virtualdevice.test", "virtualdevice"),
+                virtualDeviceId,
                 mInputController, mCameraAccessController,
                 mPendingTrampolineCallback, mActivityListener, mSoundEffectListener,
                 mRunningAppsChangedCallback, params, new DisplayManagerGlobal(mIDisplayManager));
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index e1f3c2b..57aa0b9 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -7753,7 +7753,8 @@
     public void testOnNotificationActionClick() {
         final int actionIndex = 2;
         final Notification.Action action =
-                new Notification.Action.Builder(null, "text", null).build();
+                new Notification.Action.Builder(null, "text", PendingIntent.getActivity(
+                        mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build();
         final boolean generatedByAssistant = false;
 
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
@@ -7777,7 +7778,8 @@
     public void testOnAssistantNotificationActionClick() {
         final int actionIndex = 1;
         final Notification.Action action =
-                new Notification.Action.Builder(null, "text", null).build();
+                new Notification.Action.Builder(null, "text", PendingIntent.getActivity(
+                        mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build();
         final boolean generatedByAssistant = true;
 
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
diff --git a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
index 3bb86a7..b9492e9 100644
--- a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
@@ -170,10 +170,15 @@
     }
 
     private void pressKey(int keyCode, long pressTime, boolean interactive) {
+        pressKey(keyCode, pressTime, interactive, false /* defaultDisplayOn */);
+    }
+
+    private void pressKey(
+            int keyCode, long pressTime, boolean interactive, boolean defaultDisplayOn) {
         long eventTime = SystemClock.uptimeMillis();
         final KeyEvent keyDown = new KeyEvent(eventTime, eventTime, ACTION_DOWN,
                 keyCode, 0 /* repeat */, 0 /* metaState */);
-        mDetector.interceptKey(keyDown, interactive);
+        mDetector.interceptKey(keyDown, interactive, defaultDisplayOn);
 
         // keep press down.
         try {
@@ -186,7 +191,7 @@
         final KeyEvent keyUp = new KeyEvent(eventTime, eventTime, ACTION_UP,
                 keyCode, 0 /* repeat */, 0 /* metaState */);
 
-        mDetector.interceptKey(keyUp, interactive);
+        mDetector.interceptKey(keyUp, interactive, defaultDisplayOn);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
index 16c38ac..08438c8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
@@ -354,25 +354,37 @@
             }
         });
 
-        final LandscapeActivity activity =
-                (LandscapeActivity) startTestActivity(LandscapeActivity.class);
+        final boolean isIgnoringOrientationRequest =
+                CommonUtils.getIgnoreOrientationRequest(Display.DEFAULT_DISPLAY);
+        if (isIgnoringOrientationRequest) {
+            CommonUtils.setIgnoreOrientationRequest(Display.DEFAULT_DISPLAY, false);
+        }
 
-        int[] taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue,
-                candidate -> candidate[0] == activity.getTaskId());
-        assertNotNull(taskIdAndOrientation);
-        assertEquals(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, taskIdAndOrientation[1]);
+        try {
+            final LandscapeActivity activity =
+                    (LandscapeActivity) startTestActivity(LandscapeActivity.class);
 
-        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
-        taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue,
-                candidate -> candidate[0] == activity.getTaskId());
-        assertNotNull(taskIdAndOrientation);
-        assertEquals(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, taskIdAndOrientation[1]);
+            int[] taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue,
+                    candidate -> candidate[0] == activity.getTaskId());
+            assertNotNull(taskIdAndOrientation);
+            assertEquals(
+                    ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, taskIdAndOrientation[1]);
 
-        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
-        taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue,
-                candidate -> candidate[0] == activity.getTaskId());
-        assertNotNull(taskIdAndOrientation);
-        assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, taskIdAndOrientation[1]);
+            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
+            taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue,
+                    candidate -> candidate[0] == activity.getTaskId());
+            assertNotNull(taskIdAndOrientation);
+            assertEquals(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, taskIdAndOrientation[1]);
+
+            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+            taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue,
+                    candidate -> candidate[0] == activity.getTaskId());
+            assertNotNull(taskIdAndOrientation);
+            assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, taskIdAndOrientation[1]);
+        } finally {
+            CommonUtils.setIgnoreOrientationRequest(
+                    Display.DEFAULT_DISPLAY, isIgnoringOrientationRequest);
+        }
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 07cdfaf..9b6d4e2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -39,6 +39,7 @@
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
+import static android.window.TransitionInfo.FLAG_SYNC;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 import static android.window.TransitionInfo.isIndependent;
 
@@ -2390,6 +2391,37 @@
         assertFalse(controller.isCollecting());
     }
 
+    @Test
+    public void testNoSyncFlagIfOneTrack() {
+        final TransitionController controller = mAtm.getTransitionController();
+        final TestTransitionPlayer player = registerTestTransitionPlayer();
+
+        mSyncEngine = createTestBLASTSyncEngine();
+        controller.setSyncEngine(mSyncEngine);
+
+        final Transition transitA = createTestTransition(TRANSIT_OPEN, controller);
+        final Transition transitB = createTestTransition(TRANSIT_OPEN, controller);
+        final Transition transitC = createTestTransition(TRANSIT_OPEN, controller);
+
+        controller.startCollectOrQueue(transitA, (deferred) -> {});
+        controller.startCollectOrQueue(transitB, (deferred) -> {});
+        controller.startCollectOrQueue(transitC, (deferred) -> {});
+
+        // Verify that, as-long as there is <= 1 track, we won't get a SYNC flag
+        transitA.start();
+        transitA.setAllReady();
+        mSyncEngine.tryFinishForTest(transitA.getSyncId());
+        assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0);
+        transitB.start();
+        transitB.setAllReady();
+        mSyncEngine.tryFinishForTest(transitB.getSyncId());
+        assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0);
+        transitC.start();
+        transitC.setAllReady();
+        mSyncEngine.tryFinishForTest(transitC.getSyncId());
+        assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0);
+    }
+
     private static void makeTaskOrganized(Task... tasks) {
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         for (Task t : tasks) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/CommonUtils.java b/services/tests/wmtests/src/com/android/server/wm/utils/CommonUtils.java
index ed23296..bfbba5f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/CommonUtils.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/CommonUtils.java
@@ -21,9 +21,12 @@
 import android.app.Activity;
 import android.app.KeyguardManager;
 import android.app.UiAutomation;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.Log;
+import android.view.IWindowManager;
 import android.view.KeyEvent;
+import android.view.WindowManagerGlobal;
 
 import androidx.test.uiautomator.UiDevice;
 
@@ -48,6 +51,37 @@
         }
     }
 
+    public static boolean getIgnoreOrientationRequest(int displayId) {
+        final UiDevice uiDevice = UiDevice.getInstance(getInstrumentation());
+        final String result;
+        try {
+            result = uiDevice.executeShellCommand("cmd window get-ignore-orientation-request -d "
+                    + displayId);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        final String[] tokens = result.split(" ");
+        if (tokens.length != 4) {
+            throw new RuntimeException("Expecting a result with 4 tokens, but got " + result);
+        }
+
+        // The output looks like "ignoreOrientationRequest true for displayId=0"
+        return Boolean.parseBoolean(tokens[1]);
+    }
+
+    public static void setIgnoreOrientationRequest(
+            int displayId, boolean ignoreOrientationRequest) {
+        runWithShellPermissionIdentity(() -> {
+            final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+            try {
+                wm.setIgnoreOrientationRequest(displayId, ignoreOrientationRequest);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        });
+    }
+
     /** Dismisses the Keyguard if it is locked. */
     public static void dismissKeyguard() {
         final KeyguardManager keyguardManager = getInstrumentation().getContext().getSystemService(
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 58da4b43..3d78a1d 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -54,6 +54,7 @@
 import android.service.voice.HotwordDetectionService;
 import android.service.voice.HotwordDetectionServiceFailure;
 import android.service.voice.HotwordDetector;
+import android.service.voice.IDetectorSessionStorageService;
 import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
 import android.service.voice.ISandboxedDetectionService;
 import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback;
@@ -69,6 +70,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.internal.infra.AndroidFuture;
 import com.android.internal.infra.ServiceConnector;
 import com.android.server.LocalServices;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
@@ -157,6 +159,8 @@
     @NonNull private ServiceConnection mRemoteVisualQueryDetectionService;
     @GuardedBy("mLock")
     @Nullable private IBinder mAudioFlinger;
+
+    @Nullable private IHotwordRecognitionStatusCallback mHotwordRecognitionCallback;
     @GuardedBy("mLock")
     private boolean mDebugHotwordLogging = false;
 
@@ -694,6 +698,7 @@
             updateContentCaptureManager(connection);
             updateSpeechService(connection);
             updateServiceIdentity(connection);
+            updateStorageService(connection);
             return connection;
         }
     }
@@ -910,6 +915,7 @@
                     mVoiceInteractionServiceUid, mVoiceInteractorIdentity,
                     mScheduledExecutorService, mDebugHotwordLogging, mRemoteExceptionListener);
         }
+        mHotwordRecognitionCallback = callback;
         mDetectorSessions.put(detectorType, session);
         session.initialize(options, sharedMemory);
     }
@@ -1035,6 +1041,23 @@
         }));
     }
 
+    private void updateStorageService(ServiceConnection connection) {
+        connection.run(service -> {
+            service.registerRemoteStorageService(new IDetectorSessionStorageService.Stub() {
+                @Override
+                public void openFile(String filename, AndroidFuture future)
+                        throws RemoteException {
+                    Slog.v(TAG, "BinderCallback#onFileOpen");
+                    try {
+                        mHotwordRecognitionCallback.onOpenFile(filename, future);
+                    } catch (RemoteException e) {
+                        e.rethrowFromSystemServer();
+                    }
+                }
+            });
+        });
+    }
+
     private void addServiceUidForAudioPolicy(int uid) {
         mScheduledExecutorService.execute(() -> {
             AudioManagerInternal audioManager =
diff --git a/telecomm/OWNERS b/telecomm/OWNERS
index dcaf858..b57b7c7 100644
--- a/telecomm/OWNERS
+++ b/telecomm/OWNERS
@@ -4,7 +4,6 @@
 tgunn@google.com
 xiaotonj@google.com
 rgreenwalt@google.com
-chinmayd@google.com
 grantmenke@google.com
 pmadapurmath@google.com
 tjstuart@google.com
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
new file mode 100644
index 0000000..87231c8
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 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.wm.flicker.activityembedding.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.RequiresDevice
+import android.tools.common.datatypes.Rect
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.traces.parsers.toFlickerComponent
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.utils.*
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/***
+ * Test entering System SplitScreen with Activity Embedding Split and another app.
+ *
+ * Setup: Launch A|B in split and secondaryApp, return to home.
+ * Transitions: Let AE Split A|B enter splitscreen with secondaryApp. Resulting in A|B|secondaryApp.
+ *
+ * To run this test: `atest FlickerTestsOther:EnterSystemSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class EnterSystemSplitTest(flicker: LegacyFlickerTest) :
+        ActivityEmbeddingTestBase(flicker) {
+
+    private val secondaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            testApp.launchViaIntent(wmHelper)
+            testApp.launchSecondaryActivity(wmHelper)
+            secondaryApp.launchViaIntent(wmHelper)
+            tapl.goHome()
+            wmHelper
+                    .StateSyncBuilder()
+                    .withAppTransitionIdle()
+                    .withHomeActivityVisible()
+                    .waitForAndVerify()
+            startDisplayBounds =
+                    wmHelper.currentState.layerState.physicalDisplayBounds ?:
+                    error("Display not found")
+        }
+        transitions {
+            SplitScreenUtils.enterSplit(wmHelper, tapl, device, testApp, secondaryApp)
+            SplitScreenUtils.waitForSplitComplete(wmHelper, testApp, secondaryApp)
+        }
+    }
+
+    @Presubmit
+    @Test
+    fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+    @Presubmit
+    @Test
+    fun activityEmbeddingSplitLayerBecomesVisible() {
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+                testApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
+    }
+
+    @Presubmit
+    @Test
+    fun activityEmbeddingSplitWindowBecomesVisible() = flicker.appWindowIsVisibleAtEnd(testApp)
+
+    @Presubmit
+    @Test
+    fun secondaryLayerBecomesVisible() {
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+                secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true)
+    }
+
+    @Presubmit
+    @Test
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
+
+    /**
+     * After the transition there should be both ActivityEmbedding activities,
+     * SplitScreenPrimaryActivity and the system split divider on screen.
+     * Verify the layers are in expected sizes.
+     */
+    @Presubmit
+    @Test
+    fun activityEmbeddingSplitSurfaceAreEven() {
+        flicker.assertLayersEnd {
+            val leftAELayerRegion =
+                    visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val rightAELayerRegion =
+                    visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            val secondaryAppLayerRegion =
+                    visibleRegion(
+                            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent())
+            val systemDivider = visibleRegion(SPLIT_SCREEN_DIVIDER_COMPONENT)
+            leftAELayerRegion
+                    .plus(rightAELayerRegion.region)
+                    .plus(secondaryAppLayerRegion.region)
+                    .plus(systemDivider.region)
+                    .coversExactly(startDisplayBounds)
+            check { "ActivityEmbeddingSplitHeight" }
+                    .that(leftAELayerRegion.region.height)
+                    .isEqual(rightAELayerRegion.region.height)
+            check { "SystemSplitHeight" }
+                    .that(rightAELayerRegion.region.height)
+                    .isEqual(secondaryAppLayerRegion.region.height)
+            // TODO(b/292283182): Remove this special case handling.
+            check { "ActivityEmbeddingSplitWidth" }
+                    .that(Math.abs(
+                            leftAELayerRegion.region.width - rightAELayerRegion.region.width))
+                    .isLower(2)
+            check { "SystemSplitWidth" }
+                    .that(Math.abs(secondaryAppLayerRegion.region.width -
+                            2 * rightAELayerRegion.region.width))
+                    .isLower(2)
+        }
+    }
+
+    /**
+     * Verify the windows are in expected sizes.
+     */
+    @Presubmit
+    @Test
+    fun activityEmbeddingSplitWindowsAreEven() {
+        flicker.assertWmEnd {
+            val leftAEWindowRegion =
+                    visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val rightAEWindowRegion =
+                    visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            // There's no window for the divider bar.
+            val secondaryAppLayerRegion =
+                    visibleRegion(
+                            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent())
+            check { "ActivityEmbeddingSplitHeight" }
+                    .that(leftAEWindowRegion.region.height)
+                    .isEqual(rightAEWindowRegion.region.height)
+            check { "SystemSplitHeight" }
+                    .that(rightAEWindowRegion.region.height)
+                    .isEqual(secondaryAppLayerRegion.region.height)
+            check { "ActivityEmbeddingSplitWidth" }
+                    .that(Math.abs(
+                            leftAEWindowRegion.region.width - rightAEWindowRegion.region.width))
+                    .isLower(2)
+            check { "SystemSplitWidth" }
+                    .that(Math.abs(secondaryAppLayerRegion.region.width -
+                            2 * rightAEWindowRegion.region.width))
+                    .isLower(2)
+        }
+    }
+
+    @Ignore("Not applicable to this CUJ.")
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {}
+
+    companion object {
+        /** {@inheritDoc} */
+        private var startDisplayBounds = Rect.EMPTY
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
\ No newline at end of file
diff --git a/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java
new file mode 100644
index 0000000..1b98887
--- /dev/null
+++ b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2023 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.input;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.view.InputChannel;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+import android.view.ViewConfiguration;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest FocusEventDebugViewTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class FocusEventDebugViewTest {
+
+    private FocusEventDebugView mFocusEventDebugView;
+    private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView;
+    private FocusEventDebugView.RotaryInputGraphView mRotaryInputGraphView;
+    private float mScaledVerticalScrollFactor;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        mScaledVerticalScrollFactor =
+                ViewConfiguration.get(context).getScaledVerticalScrollFactor();
+        InputManagerService mockService = mock(InputManagerService.class);
+        when(mockService.monitorInput(anyString(), anyInt()))
+                .thenReturn(InputChannel.openInputChannelPair("FocusEventDebugViewTest")[1]);
+
+        mRotaryInputValueView = new FocusEventDebugView.RotaryInputValueView(context);
+        mRotaryInputGraphView = new FocusEventDebugView.RotaryInputGraphView(context);
+        mFocusEventDebugView = new FocusEventDebugView(context, mockService,
+                () -> mRotaryInputValueView, () -> mRotaryInputGraphView);
+    }
+
+    @Test
+    public void startsRotaryInputValueViewWithDefaultValue() {
+        assertEquals("+0.0", mRotaryInputValueView.getText());
+    }
+
+    @Test
+    public void startsRotaryInputGraphViewWithDefaultFrameCenter() {
+        assertEquals(0, mRotaryInputGraphView.getFrameCenterPosition(), 0.01);
+    }
+
+    @Test
+    public void handleRotaryInput_updatesRotaryInputValueViewWithScrollValue() {
+        mFocusEventDebugView.handleUpdateShowRotaryInput(true);
+
+        mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(0.5f));
+
+        assertEquals(String.format("+%.1f", 0.5f * mScaledVerticalScrollFactor),
+                mRotaryInputValueView.getText());
+    }
+
+    @Test
+    public void handleRotaryInput_translatesRotaryInputGraphViewWithHighScrollValue() {
+        mFocusEventDebugView.handleUpdateShowRotaryInput(true);
+
+        mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(1000f));
+
+        assertTrue(mRotaryInputGraphView.getFrameCenterPosition() > 0);
+    }
+
+    @Test
+    public void updateActivityStatus_setsAndRemovesColorFilter() {
+        // It should not be active initially.
+        assertNull(mRotaryInputValueView.getBackground().getColorFilter());
+
+        mRotaryInputValueView.updateActivityStatus(true);
+        // It should be active after rotary input.
+        assertNotNull(mRotaryInputValueView.getBackground().getColorFilter());
+
+        mRotaryInputValueView.updateActivityStatus(false);
+        // It should not be active after waiting for mUpdateActivityStatusCallback.
+        assertNull(mRotaryInputValueView.getBackground().getColorFilter());
+    }
+
+    private MotionEvent createRotaryMotionEvent(float scrollAxisValue) {
+        PointerCoords pointerCoords = new PointerCoords();
+        pointerCoords.setAxisValue(MotionEvent.AXIS_SCROLL, scrollAxisValue);
+        PointerProperties pointerProperties = new PointerProperties();
+
+        return MotionEvent.obtain(
+                /* downTime */ 0,
+                /* eventTime */ 0,
+                /* action */ MotionEvent.ACTION_SCROLL,
+                /* pointerCount */ 1,
+                /* pointerProperties */ new PointerProperties[] {pointerProperties},
+                /* pointerCoords */ new PointerCoords[] {pointerCoords},
+                /* metaState */ 0,
+                /* buttonState */ 0,
+                /* xPrecision */ 0,
+                /* yPrecision */ 0,
+                /* deviceId */ 0,
+                /* edgeFlags */ 0,
+                /* source */ InputDevice.SOURCE_ROTARY_ENCODER,
+                /* flags */ 0
+        );
+    }
+}
diff --git a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java
index ba12acb..2b605c5 100644
--- a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java
+++ b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java
@@ -18,6 +18,7 @@
 
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
+
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 
@@ -41,11 +42,11 @@
 import android.view.animation.LinearInterpolator;
 import android.widget.LinearLayout;
 
+import androidx.appcompat.app.AppCompatActivity;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import androidx.appcompat.app.AppCompatActivity;
-
 public class ChatActivity extends AppCompatActivity {
 
     private View mRoot;
@@ -148,7 +149,7 @@
                 inset = min(inset, shown);
                 mAnimationController.setInsetsAndAlpha(
                         Insets.of(0, 0, 0, inset),
-                        1f, (inset - start) / (float)(end - start));
+                        1f, start == end ? 1f : (inset - start) / (float) (end - start));
             }
         });