Merge "Remove @UnsupportedAppUsage annotation from new dream API" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 48d6392..8591a9c 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -77,6 +77,7 @@
         "camera_platform_flags_core_java_lib",
         "com.android.hardware.input-aconfig-java",
         "com.android.input.flags-aconfig-java",
+        "com.android.internal.compat.flags-aconfig-java",
         "com.android.internal.foldables.flags-aconfig-java",
         "com.android.internal.pm.pkg.component.flags-aconfig-java",
         "com.android.media.flags.bettertogether-aconfig-java",
@@ -663,6 +664,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+// Platform Compat
+java_aconfig_library {
+    name: "com.android.internal.compat.flags-aconfig-java",
+    aconfig_declarations: "compat_logging_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Multi user
 aconfig_declarations {
     name: "android.multiuser.flags-aconfig",
diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp
index 0104ee1..ace56d4 100644
--- a/apex/jobscheduler/service/Android.bp
+++ b/apex/jobscheduler/service/Android.bp
@@ -20,6 +20,7 @@
     ],
 
     libs: [
+        "androidx.annotation_annotation",
         "app-compat-annotations",
         "error_prone_annotations",
         "framework",
diff --git a/core/api/current.txt b/core/api/current.txt
index 05b67ee..7552b5c0 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4482,7 +4482,7 @@
     method @CallSuper public void onActionModeStarted(android.view.ActionMode);
     method public void onActivityReenter(int, android.content.Intent);
     method protected void onActivityResult(int, int, android.content.Intent);
-    method @FlaggedApi("android.security.content_uri_permission_apis") public void onActivityResult(int, int, @NonNull android.content.Intent, @NonNull android.app.ComponentCaller);
+    method @FlaggedApi("android.security.content_uri_permission_apis") public void onActivityResult(int, int, @Nullable android.content.Intent, @NonNull android.app.ComponentCaller);
     method @Deprecated public void onAttachFragment(android.app.Fragment);
     method public void onAttachedToWindow();
     method @Deprecated public void onBackPressed();
@@ -9602,8 +9602,8 @@
     method public static android.appwidget.AppWidgetManager getInstance(android.content.Context);
     method @FlaggedApi("android.appwidget.flags.generated_previews") @Nullable public android.widget.RemoteViews getWidgetPreview(@NonNull android.content.ComponentName, @Nullable android.os.UserHandle, int);
     method public boolean isRequestPinAppWidgetSupported();
-    method public void notifyAppWidgetViewDataChanged(int[], int);
-    method public void notifyAppWidgetViewDataChanged(int, int);
+    method @Deprecated public void notifyAppWidgetViewDataChanged(int[], int);
+    method @Deprecated public void notifyAppWidgetViewDataChanged(int, int);
     method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews);
     method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews);
     method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int);
@@ -28054,7 +28054,7 @@
     method public void sendSigningResult(@NonNull String, @NonNull byte[]);
     method public void sendTrackInfoList(@Nullable java.util.List<android.media.tv.TvTrackInfo>);
     method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.TvAdCallback);
-    method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener);
+    method public void setOnUnhandledInputEventListener(@NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener);
     method public boolean setTvView(@Nullable android.media.tv.TvView);
     method public void startAdService();
     method public void stopAdService();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fb2a4ac..4b04d10 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4912,7 +4912,6 @@
     method @FlaggedApi("com.android.internal.camera.flags.concert_mode") public int getImageFormat();
     method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public android.util.Size getSize();
     method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public android.view.Surface getSurface();
-    method @FlaggedApi("com.android.internal.camera.flags.extension_10_bit") public void setColorSpace(int);
     method @FlaggedApi("com.android.internal.camera.flags.extension_10_bit") public void setDynamicRangeProfile(long);
   }
 
@@ -4923,6 +4922,7 @@
 
   @FlaggedApi("com.android.internal.camera.flags.concert_mode") public class ExtensionConfiguration {
     ctor @FlaggedApi("com.android.internal.camera.flags.concert_mode") public ExtensionConfiguration(int, int, @NonNull java.util.List<android.hardware.camera2.extension.ExtensionOutputConfiguration>, @Nullable android.hardware.camera2.CaptureRequest);
+    method @FlaggedApi("com.android.internal.camera.flags.extension_10_bit") public void setColorSpace(int);
   }
 
   @FlaggedApi("com.android.internal.camera.flags.concert_mode") public class ExtensionOutputConfiguration {
@@ -6261,7 +6261,7 @@
     method public void addOnCompleteListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.OnCompleteListener);
     method public void addOnCompleteListener(@NonNull android.hardware.radio.ProgramList.OnCompleteListener);
     method public void close();
-    method @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier);
+    method @Deprecated @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier);
     method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull public java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramInfos(@NonNull android.hardware.radio.ProgramSelector.Identifier);
     method public void registerListCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.ListCallback);
     method public void registerListCallback(@NonNull android.hardware.radio.ProgramList.ListCallback);
@@ -6313,7 +6313,7 @@
     field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5
     field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5
     field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa
-    field public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb
+    field @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb
     field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9
     field public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; // 0x3
     field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int IDENTIFIER_TYPE_HD_STATION_LOCATION = 15; // 0xf
@@ -6321,8 +6321,8 @@
     field @Deprecated public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; // 0x4
     field public static final int IDENTIFIER_TYPE_INVALID = 0; // 0x0
     field public static final int IDENTIFIER_TYPE_RDS_PI = 2; // 0x2
-    field public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd
-    field public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc
+    field @Deprecated public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd
+    field @Deprecated public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc
     field public static final int IDENTIFIER_TYPE_VENDOR_END = 1999; // 0x7cf
     field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = 1999; // 0x7cf
     field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = 1000; // 0x3e8
@@ -6375,7 +6375,7 @@
     field public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8; // 0x8
     field public static final int CONFIG_DAB_FM_LINKING = 7; // 0x7
     field public static final int CONFIG_DAB_FM_SOFT_LINKING = 9; // 0x9
-    field public static final int CONFIG_FORCE_ANALOG = 2; // 0x2
+    field @Deprecated public static final int CONFIG_FORCE_ANALOG = 2; // 0x2
     field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_AM = 11; // 0xb
     field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_FM = 10; // 0xa
     field public static final int CONFIG_FORCE_DIGITAL = 3; // 0x3
diff --git a/core/java/Android.bp b/core/java/Android.bp
index ab1c9a4..4f96206 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -167,6 +167,9 @@
         "com/android/internal/logging/UiEventLoggerImpl.java",
         ":statslog-framework-java-gen",
     ],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
     static_libs: ["modules-utils-uieventlogger-interface"],
 }
 
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index afbefca..1cc2d25 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -7473,7 +7473,7 @@
      *               intent.
      */
     @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
-    public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data,
+    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data,
             @NonNull ComponentCaller caller) {
         onActivityResult(requestCode, resultCode, data);
     }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 1d39186..ae5cacd 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -974,6 +974,7 @@
         ContentCaptureOptions contentCaptureOptions;
 
         long[] disabledCompatChanges;
+        long[] mLoggableCompatChanges;
 
         SharedMemory mSerializedSystemFontMap;
 
@@ -1283,6 +1284,7 @@
                 AutofillOptions autofillOptions,
                 ContentCaptureOptions contentCaptureOptions,
                 long[] disabledCompatChanges,
+                long[] loggableCompatChanges,
                 SharedMemory serializedSystemFontMap,
                 long startRequestedElapsedTime,
                 long startRequestedUptime) {
@@ -1337,6 +1339,7 @@
             data.autofillOptions = autofillOptions;
             data.contentCaptureOptions = contentCaptureOptions;
             data.disabledCompatChanges = disabledCompatChanges;
+            data.mLoggableCompatChanges = loggableCompatChanges;
             data.mSerializedSystemFontMap = serializedSystemFontMap;
             data.startRequestedElapsedTime = startRequestedElapsedTime;
             data.startRequestedUptime = startRequestedUptime;
@@ -4073,6 +4076,13 @@
                     ActivityManager.getService().waitForNetworkStateUpdate(mNetworkBlockSeq);
                     mNetworkBlockSeq = INVALID_PROC_STATE_SEQ;
                 } catch (RemoteException ignored) {}
+                if (Flags.clearDnsCacheOnNetworkRulesUpdate()) {
+                    // InetAddress will cache UnknownHostException failures. If the rules got
+                    // updated and the app has network access now, we need to clear the negative
+                    // cache to ensure valid dns queries can work immediately.
+                    // TODO: b/329133769 - Clear only the negative cache once it is available.
+                    InetAddress.clearDnsCache();
+                }
             }
         }
     }
@@ -7123,7 +7133,7 @@
         Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis(),
                 data.startRequestedElapsedTime, data.startRequestedUptime);
 
-        AppCompatCallbacks.install(data.disabledCompatChanges);
+        AppCompatCallbacks.install(data.disabledCompatChanges, data.mLoggableCompatChanges);
         // Let libcore handle any compat changes after installing the list of compat changes.
         AppSpecializationHooks.handleCompatChangesBeforeBindingApplication();
 
diff --git a/core/java/android/app/AppCompatCallbacks.java b/core/java/android/app/AppCompatCallbacks.java
index 134cef5..f2debfc 100644
--- a/core/java/android/app/AppCompatCallbacks.java
+++ b/core/java/android/app/AppCompatCallbacks.java
@@ -30,41 +30,59 @@
  */
 public final class AppCompatCallbacks implements Compatibility.BehaviorChangeDelegate {
     private final long[] mDisabledChanges;
+    private final long[] mLoggableChanges;
     private final ChangeReporter mChangeReporter;
 
     /**
-     * Install this class into the current process.
+     * Install this class into the current process using the disabled and loggable changes lists.
      *
      * @param disabledChanges Set of compatibility changes that are disabled for this process.
+     * @param loggableChanges Set of compatibility changes that we want to log.
      */
-    public static void install(long[] disabledChanges) {
-        Compatibility.setBehaviorChangeDelegate(new AppCompatCallbacks(disabledChanges));
+    public static void install(long[] disabledChanges, long[] loggableChanges) {
+        Compatibility.setBehaviorChangeDelegate(
+                new AppCompatCallbacks(disabledChanges, loggableChanges));
     }
 
-    private AppCompatCallbacks(long[] disabledChanges) {
+    private AppCompatCallbacks(long[] disabledChanges, long[] loggableChanges) {
         mDisabledChanges = Arrays.copyOf(disabledChanges, disabledChanges.length);
+        mLoggableChanges = Arrays.copyOf(loggableChanges, loggableChanges.length);
         Arrays.sort(mDisabledChanges);
-        mChangeReporter = new ChangeReporter(
-                ChangeReporter.SOURCE_APP_PROCESS);
+        Arrays.sort(mLoggableChanges);
+        mChangeReporter = new ChangeReporter(ChangeReporter.SOURCE_APP_PROCESS);
+    }
+
+    /**
+     * Helper to determine if a list contains a changeId.
+     *
+     * @param list to search through
+     * @param changeId for which to search in the list
+     * @return true if the given changeId is found in the provided array.
+     */
+    private boolean changeIdInChangeList(long[] list, long changeId) {
+        return Arrays.binarySearch(list, changeId) >= 0;
     }
 
     public void onChangeReported(long changeId) {
-        reportChange(changeId, ChangeReporter.STATE_LOGGED);
+        boolean isLoggable = changeIdInChangeList(mLoggableChanges, changeId);
+        reportChange(changeId, ChangeReporter.STATE_LOGGED, isLoggable);
     }
 
     public boolean isChangeEnabled(long changeId) {
-        if (Arrays.binarySearch(mDisabledChanges, changeId) < 0) {
-            // Not present in the disabled array
-            reportChange(changeId, ChangeReporter.STATE_ENABLED);
+        boolean isEnabled = !changeIdInChangeList(mDisabledChanges, changeId);
+        boolean isLoggable = changeIdInChangeList(mLoggableChanges, changeId);
+        if (isEnabled) {
+            // Not present in the disabled changeId array
+            reportChange(changeId, ChangeReporter.STATE_ENABLED, isLoggable);
             return true;
         }
-        reportChange(changeId, ChangeReporter.STATE_DISABLED);
+        reportChange(changeId, ChangeReporter.STATE_DISABLED, isLoggable);
         return false;
     }
 
-    private void reportChange(long changeId, int state) {
+    private void reportChange(long changeId, int state, boolean isLoggable) {
         int uid = Process.myUid();
-        mChangeReporter.reportChange(uid, changeId, state);
+        mChangeReporter.reportChange(uid, changeId, state, isLoggable);
     }
 
 }
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index af56cb4..ed0c933 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -272,10 +272,17 @@
 
     @UnsupportedAppUsage
     private Context mOuterContext;
+
+    private final Object mThemeLock = new Object();
+
     @UnsupportedAppUsage
+    @GuardedBy("mThemeLock")
     private int mThemeResource = 0;
+
     @UnsupportedAppUsage
+    @GuardedBy("mThemeLock")
     private Resources.Theme mTheme = null;
+
     @UnsupportedAppUsage
     private PackageManager mPackageManager;
     private Context mReceiverRestrictedContext = null;
@@ -288,7 +295,6 @@
 
     private ContentCaptureOptions mContentCaptureOptions = null;
 
-    private final Object mSync = new Object();
     /**
      * Indicates this {@link Context} can not handle UI components properly and is not associated
      * with a {@link Display} instance.
@@ -340,21 +346,18 @@
      */
     private boolean mOwnsToken = false;
 
-    @GuardedBy("mSync")
-    private File mDatabasesDir;
-    @GuardedBy("mSync")
-    @UnsupportedAppUsage
-    private File mPreferencesDir;
-    @GuardedBy("mSync")
-    private File mFilesDir;
-    @GuardedBy("mSync")
-    private File mCratesDir;
-    @GuardedBy("mSync")
-    private File mNoBackupFilesDir;
-    @GuardedBy("mSync")
-    private File mCacheDir;
-    @GuardedBy("mSync")
-    private File mCodeCacheDir;
+    private final Object mDirsLock = new Object();
+    private volatile File mDatabasesDir;
+    @UnsupportedAppUsage private volatile File mPreferencesDir;
+    private volatile File mFilesDir;
+    private volatile File mCratesDir;
+    private volatile File mNoBackupFilesDir;
+    private volatile File[] mExternalFilesDirs;
+    private volatile File[] mObbDirs;
+    private volatile File mCacheDir;
+    private volatile File mCodeCacheDir;
+    private volatile File[] mExternalCacheDirs;
+    private volatile File[] mExternalMediaDirs;
 
     // The system service cache for the system services that are cached per-ContextImpl.
     @UnsupportedAppUsage
@@ -458,7 +461,7 @@
 
     @Override
     public void setTheme(int resId) {
-        synchronized (mSync) {
+        synchronized (mThemeLock) {
             if (mThemeResource != resId) {
                 mThemeResource = resId;
                 initializeTheme();
@@ -468,14 +471,14 @@
 
     @Override
     public int getThemeResId() {
-        synchronized (mSync) {
+        synchronized (mThemeLock) {
             return mThemeResource;
         }
     }
 
     @Override
     public Resources.Theme getTheme() {
-        synchronized (mSync) {
+        synchronized (mThemeLock) {
             if (mTheme != null) {
                 return mTheme;
             }
@@ -488,6 +491,7 @@
         }
     }
 
+    @GuardedBy("mThemeLock")
     private void initializeTheme() {
         if (mTheme == null) {
             mTheme = mResources.newTheme();
@@ -597,12 +601,18 @@
             if (sp == null) {
                 checkMode(mode);
                 if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
-                    if (isCredentialProtectedStorage()
-                            && !getSystemService(UserManager.class)
-                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
-                        throw new IllegalStateException("SharedPreferences in credential encrypted "
-                                + "storage are not available until after user (id "
-                                + UserHandle.myUserId() + ") is unlocked");
+                    if (isCredentialProtectedStorage()) {
+                        final UserManager um = getSystemService(UserManager.class);
+                        if (um == null) {
+                            throw new IllegalStateException("SharedPreferences cannot be accessed "
+                                    + "if UserManager is not available. "
+                                    + "(e.g. from inside an isolated process)");
+                        }
+                        if (!um.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
+                            throw new IllegalStateException("SharedPreferences in "
+                                    + "credential encrypted storage are not available until after "
+                                    + "user (id " + UserHandle.myUserId() + ") is unlocked");
+                        }
                     }
                 }
                 sp = new SharedPreferencesImpl(file, mode);
@@ -731,12 +741,18 @@
 
     @UnsupportedAppUsage
     private File getPreferencesDir() {
-        synchronized (mSync) {
-            if (mPreferencesDir == null) {
-                mPreferencesDir = new File(getDataDir(), "shared_prefs");
+        File localPreferencesDir = mPreferencesDir;
+        if (localPreferencesDir == null) {
+            synchronized (mDirsLock) {
+                localPreferencesDir = mPreferencesDir;
+                if (localPreferencesDir == null) {
+                    localPreferencesDir = new File(getDataDir(), "shared_prefs");
+                    ensurePrivateDirExists(localPreferencesDir);
+                    mPreferencesDir = localPreferencesDir;
+                }
             }
-            return ensurePrivateDirExists(mPreferencesDir);
         }
+        return localPreferencesDir;
     }
 
     @Override
@@ -778,16 +794,16 @@
     /**
      * Common-path handling of app data dir creation
      */
-    private static File ensurePrivateDirExists(File file) {
-        return ensurePrivateDirExists(file, 0771, -1, null);
+    private static void ensurePrivateDirExists(File file) {
+        ensurePrivateDirExists(file, 0771, -1, null);
     }
 
-    private static File ensurePrivateCacheDirExists(File file, String xattr) {
+    private static void ensurePrivateCacheDirExists(File file, String xattr) {
         final int gid = UserHandle.getCacheAppGid(Process.myUid());
-        return ensurePrivateDirExists(file, 02771, gid, xattr);
+        ensurePrivateDirExists(file, 02771, gid, xattr);
     }
 
-    private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
+    private static void ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
         if (!file.exists()) {
             final String path = file.getAbsolutePath();
             try {
@@ -815,17 +831,22 @@
                 }
             }
         }
-        return file;
     }
 
     @Override
     public File getFilesDir() {
-        synchronized (mSync) {
-            if (mFilesDir == null) {
-                mFilesDir = new File(getDataDir(), "files");
+        File localFilesDir = mFilesDir;
+        if (localFilesDir == null) {
+            localFilesDir = mFilesDir;
+            synchronized (mDirsLock) {
+                if (localFilesDir == null) {
+                    localFilesDir = new File(getDataDir(), "files");
+                    ensurePrivateDirExists(localFilesDir);
+                    mFilesDir = localFilesDir;
+                }
             }
-            return ensurePrivateDirExists(mFilesDir);
         }
+        return localFilesDir;
     }
 
     @Override
@@ -835,25 +856,37 @@
         final Path absoluteNormalizedCratePath = cratesRootPath.resolve(crateId)
                 .toAbsolutePath().normalize();
 
-        synchronized (mSync) {
-            if (mCratesDir == null) {
-                mCratesDir = cratesRootPath.toFile();
+        File localCratesDir = mCratesDir;
+        if (localCratesDir == null) {
+            synchronized (mDirsLock) {
+                localCratesDir = mCratesDir;
+                if (localCratesDir == null) {
+                    localCratesDir = cratesRootPath.toFile();
+                    ensurePrivateDirExists(localCratesDir);
+                    mCratesDir = localCratesDir;
+                }
             }
-            ensurePrivateDirExists(mCratesDir);
         }
 
-        File cratedDir = absoluteNormalizedCratePath.toFile();
-        return ensurePrivateDirExists(cratedDir);
+        File crateDir = absoluteNormalizedCratePath.toFile();
+        ensurePrivateDirExists(crateDir);
+        return crateDir;
     }
 
     @Override
     public File getNoBackupFilesDir() {
-        synchronized (mSync) {
-            if (mNoBackupFilesDir == null) {
-                mNoBackupFilesDir = new File(getDataDir(), "no_backup");
+        File localNoBackupFilesDir = mNoBackupFilesDir;
+        if (localNoBackupFilesDir == null) {
+            synchronized (mDirsLock) {
+                localNoBackupFilesDir = mNoBackupFilesDir;
+                if (localNoBackupFilesDir == null) {
+                    localNoBackupFilesDir = new File(getDataDir(), "no_backup");
+                    ensurePrivateDirExists(localNoBackupFilesDir);
+                    mNoBackupFilesDir = localNoBackupFilesDir;
+                }
             }
-            return ensurePrivateDirExists(mNoBackupFilesDir);
         }
+        return localNoBackupFilesDir;
     }
 
     @Override
@@ -865,13 +898,24 @@
 
     @Override
     public File[] getExternalFilesDirs(String type) {
-        synchronized (mSync) {
-            File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
-            if (type != null) {
-                dirs = Environment.buildPaths(dirs, type);
+        File[] localExternalFilesDirs = mExternalFilesDirs;
+        if (localExternalFilesDirs == null) {
+            synchronized (mDirsLock) {
+                localExternalFilesDirs = mExternalFilesDirs;
+                if (localExternalFilesDirs == null) {
+                    localExternalFilesDirs =
+                            Environment.buildExternalStorageAppFilesDirs(getPackageName());
+                    if (type != null) {
+                        localExternalFilesDirs =
+                                Environment.buildPaths(localExternalFilesDirs, type);
+                    }
+                    localExternalFilesDirs = ensureExternalDirsExistOrFilter(
+                            localExternalFilesDirs, true /* tryCreateInProcess */);
+                    mExternalFilesDirs = localExternalFilesDirs;
+                }
             }
-            return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */);
         }
+        return localExternalFilesDirs;
     }
 
     @Override
@@ -883,30 +927,51 @@
 
     @Override
     public File[] getObbDirs() {
-        synchronized (mSync) {
-            File[] dirs = Environment.buildExternalStorageAppObbDirs(getPackageName());
-            return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */);
+        File[] localObbDirs = mObbDirs;
+        if (mObbDirs == null) {
+            synchronized (mDirsLock) {
+                localObbDirs = mObbDirs;
+                if (localObbDirs == null) {
+                    localObbDirs = Environment.buildExternalStorageAppObbDirs(getPackageName());
+                    localObbDirs = ensureExternalDirsExistOrFilter(
+                            localObbDirs, true /* tryCreateInProcess */);
+                    mObbDirs = localObbDirs;
+                }
+            }
         }
+        return localObbDirs;
     }
 
     @Override
     public File getCacheDir() {
-        synchronized (mSync) {
-            if (mCacheDir == null) {
-                mCacheDir = new File(getDataDir(), "cache");
+        File localCacheDir = mCacheDir;
+        if (localCacheDir == null) {
+            synchronized (mDirsLock) {
+                localCacheDir = mCacheDir;
+                if (localCacheDir == null) {
+                    localCacheDir = new File(getDataDir(), "cache");
+                    ensurePrivateCacheDirExists(localCacheDir, XATTR_INODE_CACHE);
+                    mCacheDir = localCacheDir;
+                }
             }
-            return ensurePrivateCacheDirExists(mCacheDir, XATTR_INODE_CACHE);
         }
+        return localCacheDir;
     }
 
     @Override
     public File getCodeCacheDir() {
-        synchronized (mSync) {
-            if (mCodeCacheDir == null) {
-                mCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir());
+        File localCodeCacheDir = mCodeCacheDir;
+        if (localCodeCacheDir == null) {
+            synchronized (mDirsLock) {
+                localCodeCacheDir = mCodeCacheDir;
+                if (localCodeCacheDir == null) {
+                    localCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir());
+                    ensurePrivateCacheDirExists(localCodeCacheDir, XATTR_INODE_CODE_CACHE);
+                    mCodeCacheDir = localCodeCacheDir;
+                }
             }
-            return ensurePrivateCacheDirExists(mCodeCacheDir, XATTR_INODE_CODE_CACHE);
         }
+        return localCodeCacheDir;
     }
 
     /**
@@ -927,21 +992,37 @@
 
     @Override
     public File[] getExternalCacheDirs() {
-        synchronized (mSync) {
-            File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName());
-            // We don't try to create cache directories in-process, because they need special
-            // setup for accurate quota tracking. This ensures the cache dirs are always
-            // created through StorageManagerService.
-            return ensureExternalDirsExistOrFilter(dirs, false /* tryCreateInProcess */);
+        File[] localExternalCacheDirs = mExternalCacheDirs;
+        if (localExternalCacheDirs == null) {
+            synchronized (mDirsLock) {
+                localExternalCacheDirs = mExternalCacheDirs;
+                if (localExternalCacheDirs == null) {
+                    localExternalCacheDirs =
+                            Environment.buildExternalStorageAppCacheDirs(getPackageName());
+                    localExternalCacheDirs = ensureExternalDirsExistOrFilter(
+                            localExternalCacheDirs, false /* tryCreateInProcess */);
+                    mExternalCacheDirs = localExternalCacheDirs;
+                }
+            }
         }
+        return localExternalCacheDirs;
     }
 
     @Override
     public File[] getExternalMediaDirs() {
-        synchronized (mSync) {
-            File[] dirs = Environment.buildExternalStorageAppMediaDirs(getPackageName());
-            return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */);
+        File[] localExternalMediaDirs = mExternalMediaDirs;
+        if (localExternalMediaDirs == null) {
+            synchronized (mDirsLock) {
+                localExternalMediaDirs = mExternalMediaDirs;
+                if (localExternalMediaDirs == null) {
+                    localExternalMediaDirs = Environment.buildExternalStorageAppMediaDirs(getPackageName());
+                    localExternalMediaDirs = ensureExternalDirsExistOrFilter(
+                            localExternalMediaDirs, true /* tryCreateInProcess */);
+                    mExternalMediaDirs = localExternalMediaDirs;
+                }
+            }
         }
+        return localExternalMediaDirs;
     }
 
     /**
@@ -1040,16 +1121,22 @@
     }
 
     private File getDatabasesDir() {
-        synchronized (mSync) {
-            if (mDatabasesDir == null) {
-                if ("android".equals(getPackageName())) {
-                    mDatabasesDir = new File("/data/system");
-                } else {
-                    mDatabasesDir = new File(getDataDir(), "databases");
+        File localDatabasesDir = mDatabasesDir;
+        if (localDatabasesDir == null) {
+            synchronized (mDirsLock) {
+                localDatabasesDir = mDatabasesDir;
+                if (localDatabasesDir == null) {
+                    if ("android".equals(getPackageName())) {
+                        localDatabasesDir = new File("/data/system");
+                    } else {
+                        localDatabasesDir = new File(getDataDir(), "databases");
+                    }
+                    ensurePrivateDirExists(localDatabasesDir);
+                    mDatabasesDir = localDatabasesDir;
                 }
             }
-            return ensurePrivateDirExists(mDatabasesDir);
         }
+        return localDatabasesDir;
     }
 
     @Override
diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java
index 4ce983f..3e7d665 100644
--- a/core/java/android/app/GrammaticalInflectionManager.java
+++ b/core/java/android/app/GrammaticalInflectionManager.java
@@ -125,7 +125,10 @@
     /**
      * Get the current grammatical gender of privileged application from the encrypted file.
      *
-     * @return the value of grammatical gender
+     * @return the value of system grammatical gender only if the calling app has the permission,
+     * otherwise throwing an exception.
+     *
+     * @throws SecurityException if the caller does not have the required permission.
      *
      * @see Configuration#getGrammaticalGender
      */
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index a04620c..251e4e8 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -90,7 +90,7 @@
             in CompatibilityInfo compatInfo, in Map services,
             in Bundle coreSettings, in String buildSerial, in AutofillOptions autofillOptions,
             in ContentCaptureOptions contentCaptureOptions, in long[] disabledCompatChanges,
-            in SharedMemory serializedSystemFontMap,
+            in long[] loggableCompatChanges, in SharedMemory serializedSystemFontMap,
             long startRequestedElapsedTime, long startRequestedUptime);
     void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs);
     void scheduleExit();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 79cb09d..cd4c0bc 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -7531,6 +7531,9 @@
         /**
          * Calls {@link android.app.Notification.Builder#build()} on the Builder this Style is
          * attached to.
+         * <p>
+         * Note: Calling build() multiple times returns the same Notification instance,
+         * so reusing a builder to create multiple Notifications is discouraged.
          *
          * @return the fully constructed Notification.
          */
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 986205a..9ef8b38 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -189,10 +189,13 @@
     @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
     public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;
 
+    /**
+     * @hide
+     */
     @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED,
             HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER})
     @Retention(RetentionPolicy.SOURCE)
-    private @interface HeadlessDeviceOwnerMode {}
+    public @interface HeadlessDeviceOwnerMode {}
 
     /** @hide */
     public static class PolicyInfo {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 620bbaf..cb4ed058 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -53,6 +53,7 @@
 import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY;
 import static android.Manifest.permission.SET_TIME;
 import static android.Manifest.permission.SET_TIME_ZONE;
+import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
 import static android.app.admin.flags.Flags.FLAG_DEVICE_THEFT_API_ENABLED;
 import static android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED;
 import static android.app.admin.flags.Flags.FLAG_DEVICE_POLICY_SIZE_TRACKING_ENABLED;
@@ -93,6 +94,7 @@
 import android.app.IServiceConnection;
 import android.app.KeyguardManager;
 import android.app.admin.SecurityLog.SecurityEvent;
+import android.app.admin.flags.Flags;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
@@ -17526,4 +17528,25 @@
         }
         return -1;
     }
+
+    /**
+     * @return The headless device owner mode for the current set DO, returns
+     * {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED} if no DO is set.
+     *
+     * @hide
+     */
+    @DeviceAdminInfo.HeadlessDeviceOwnerMode
+    public int getHeadlessDeviceOwnerMode() {
+        if (!Flags.headlessDeviceOwnerProvisioningFixEnabled()) {
+            return HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
+        }
+        if (mService != null) {
+            try {
+                return mService.getHeadlessDeviceOwnerMode(mContext.getPackageName());
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
+    }
 }
\ No newline at end of file
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 3a7a891c..03d0b0f 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -625,4 +625,6 @@
 
     void setMaxPolicyStorageLimit(String packageName, int storageLimit);
     int getMaxPolicyStorageLimit(String packageName);
+
+    int getHeadlessDeviceOwnerMode(String callerPackageName);
 }
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 1927019..c29ea6d 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -163,3 +163,13 @@
   description: "Enable UX changes for esim management"
   bug: "295301164"
 }
+
+flag {
+  name: "headless_device_owner_provisioning_fix_enabled"
+  namespace: "enterprise"
+  description: "Fix provisioning for single-user headless DO"
+  bug: "289515470"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/app/network-policy.aconfig b/core/java/android/app/network-policy.aconfig
new file mode 100644
index 0000000..88f386f
--- /dev/null
+++ b/core/java/android/app/network-policy.aconfig
@@ -0,0 +1,11 @@
+package: "android.app"
+
+flag {
+     namespace: "backstage_power"
+     name: "clear_dns_cache_on_network_rules_update"
+     description: "Clears the DNS cache when the network rules update"
+     bug: "237556596"
+     metadata {
+       purpose: PURPOSE_BUGFIX
+     }
+}
\ No newline at end of file
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index cda4d89..2c0e035 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -822,7 +822,18 @@
      *
      * @param appWidgetIds  The AppWidget instances to notify of view data changes.
      * @param viewId        The collection view id.
+     * @deprecated The corresponding API
+     * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been
+     * deprecated. Moving forward please use
+     * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)}
+     * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote
+     * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)},
+     * {@link #updateAppWidget(int, RemoteViews)},
+     * {@link #updateAppWidget(ComponentName, RemoteViews)},
+     * {@link #partiallyUpdateAppWidget(int[], RemoteViews)},
+     * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable.
      */
+    @Deprecated
     public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) {
         if (mService == null) {
             return;
@@ -873,7 +884,18 @@
      *
      * @param appWidgetId  The AppWidget instance to notify of view data changes.
      * @param viewId       The collection view id.
+     * @deprecated The corresponding API
+     * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been
+     * deprecated. Moving forward please use
+     * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)}
+     * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote
+     * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)},
+     * {@link #updateAppWidget(int, RemoteViews)},
+     * {@link #updateAppWidget(ComponentName, RemoteViews)},
+     * {@link #partiallyUpdateAppWidget(int[], RemoteViews)},
+     * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable.
      */
+    @Deprecated
     public void notifyAppWidgetViewDataChanged(int appWidgetId, int viewId) {
         if (mService == null) {
             return;
diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl
index 533fa51..55957bf 100644
--- a/core/java/android/content/pm/ILauncherApps.aidl
+++ b/core/java/android/content/pm/ILauncherApps.aidl
@@ -133,4 +133,7 @@
     void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation);
 
     List<UserHandle> getUserProfiles();
+
+    /** Saves view capture data to the wm trace directory. */
+    void saveViewCaptureData();
 }
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 41c1f17..3a5383d 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -1328,6 +1328,19 @@
     }
 
     /**
+     * Saves view capture data to the default location.
+     * @hide
+     */
+    @RequiresPermission(READ_FRAME_BUFFER)
+    public void saveViewCaptureData() {
+        try {
+            mService.saveViewCaptureData();
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
      * Unregister a callback, so that it won't be called when LauncherApps dumps.
      * @hide
      */
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index f660770..7fd0b03 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -38,7 +38,7 @@
     name: "nine_patch_frro"
     namespace: "resource_manager"
     description: "Feature flag for creating an frro from a 9-patch"
-    bug: "309232726"
+    bug: "296324826"
 }
 
 flag {
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 749f218..083d49f 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -180,6 +180,16 @@
             EXTENSION_HDR,
             EXTENSION_NIGHT};
 
+    /**
+     * List of synthetic CameraCharacteristics keys that are supported in the extensions.
+     */
+    private static final List<CameraCharacteristics.Key>
+            SUPPORTED_SYNTHETIC_CAMERA_CHARACTERISTICS =
+            Arrays.asList(
+                    CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES,
+                    CameraCharacteristics.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES
+            );
+
     private final Context mContext;
     private final String mCameraId;
     private final Map<String, CameraCharacteristics> mCharacteristicsMap;
@@ -874,11 +884,17 @@
                 Class<CameraCharacteristics.Key<?>> keyTyped =
                         (Class<CameraCharacteristics.Key<?>>) key;
 
-                // Do not include synthetic keys. Including synthetic keys leads to undefined
-                // behavior. This causes inclusion of capabilities that may not be supported in
-                // camera extensions.
                 ret.addAll(chars.getAvailableKeyList(CameraCharacteristics.class, keyTyped, keys,
                         /*includeSynthetic*/ false));
+
+                // Add synthetic keys to the available key list if they are part of the supported
+                // synthetic camera characteristic key list
+                for (CameraCharacteristics.Key charKey :
+                        SUPPORTED_SYNTHETIC_CAMERA_CHARACTERISTICS) {
+                    if (chars.get(charKey) != null) {
+                        ret.add(charKey);
+                    }
+                }
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to query the extension for all available keys! Extension "
@@ -990,6 +1006,7 @@
                     case ImageFormat.YUV_420_888:
                     case ImageFormat.JPEG:
                     case ImageFormat.JPEG_R:
+                    case ImageFormat.YCBCR_P010:
                         break;
                     default:
                         throw new IllegalArgumentException("Unsupported format: " + format);
@@ -1021,8 +1038,9 @@
                     return generateJpegSupportedSizes(
                             extenders.second.getSupportedPostviewResolutions(sz),
                                     streamMap);
-                }  else if (format == ImageFormat.JPEG_R) {
-                    // Jpeg_R/UltraHDR is currently not supported in the basic extension case
+                }  else if (format == ImageFormat.JPEG_R || format == ImageFormat.YCBCR_P010) {
+                    // Jpeg_R/UltraHDR + YCBCR_P010 is currently not supported in the basic
+                    // extension case
                     return new ArrayList<>();
                 } else {
                     throw new IllegalArgumentException("Unsupported format: " + format);
@@ -1118,16 +1136,16 @@
      *
      * <p>Device-specific extensions currently support at most three
      * multi-frame capture surface formats. ImageFormat.JPEG will be supported by all
-     * extensions while ImageFormat.YUV_420_888 and ImageFormat.JPEG_R may or may not be
-     * supported.</p>
+     * extensions while ImageFormat.YUV_420_888, ImageFormat.JPEG_R, or ImageFormat.YCBCR_P010
+     * may or may not be supported.</p>
      *
      * @param extension the extension type
      * @param format    device-specific extension output format
      * @return non-modifiable list of available sizes or an empty list if the format is not
      * supported.
      * @throws IllegalArgumentException in case of format different from ImageFormat.JPEG,
-     *                                  ImageFormat.YUV_420_888, ImageFormat.JPEG_R; or
-     *                                  unsupported extension.
+     *                                  ImageFormat.YUV_420_888, ImageFormat.JPEG_R,
+     *                                  ImageFormat.YCBCR_P010; or unsupported extension.
      */
     public @NonNull
     List<Size> getExtensionSupportedSizes(@Extension int extension, int format) {
@@ -1151,6 +1169,7 @@
                         case ImageFormat.YUV_420_888:
                         case ImageFormat.JPEG:
                         case ImageFormat.JPEG_R:
+                        case ImageFormat.YCBCR_P010:
                             break;
                         default:
                             throw new IllegalArgumentException("Unsupported format: " + format);
@@ -1183,8 +1202,9 @@
                         } else {
                             return generateSupportedSizes(null, format, streamMap);
                         }
-                    } else if (format == ImageFormat.JPEG_R) {
-                        // Jpeg_R/UltraHDR is currently not supported in the basic extension case
+                    } else if (format == ImageFormat.JPEG_R || format == ImageFormat.YCBCR_P010) {
+                        // Jpeg_R/UltraHDR + YCBCR_P010 is currently not supported in the
+                        // basic extension case
                         return new ArrayList<>();
                     } else {
                         throw new IllegalArgumentException("Unsupported format: " + format);
@@ -1213,7 +1233,8 @@
      * @return the range of estimated minimal and maximal capture latency in milliseconds
      * or null if no capture latency info can be provided
      * @throws IllegalArgumentException in case of format different from {@link ImageFormat#JPEG},
-     *                                  {@link ImageFormat#YUV_420_888}, {@link ImageFormat#JPEG_R};
+     *                                  {@link ImageFormat#YUV_420_888}, {@link ImageFormat#JPEG_R}
+     *                                  {@link ImageFormat#YCBCR_P010};
      *                                  or unsupported extension.
      */
     public @Nullable Range<Long> getEstimatedCaptureLatencyRangeMillis(@Extension int extension,
@@ -1222,6 +1243,7 @@
             case ImageFormat.YUV_420_888:
             case ImageFormat.JPEG:
             case ImageFormat.JPEG_R:
+            case ImageFormat.YCBCR_P010:
                 //No op
                 break;
             default:
@@ -1269,8 +1291,8 @@
                     // specific and cannot be estimated accurately enough.
                     return  null;
                 }
-                if (format == ImageFormat.JPEG_R) {
-                    // JpegR/UltraHDR is not supported for basic extensions
+                if (format == ImageFormat.JPEG_R || format == ImageFormat.YCBCR_P010) {
+                    // JpegR/UltraHDR + YCBCR_P010 is not supported for basic extensions
                     return null;
                 }
 
diff --git a/core/java/android/hardware/camera2/extension/AdvancedExtender.java b/core/java/android/hardware/camera2/extension/AdvancedExtender.java
index 4895f38..8fa09a8 100644
--- a/core/java/android/hardware/camera2/extension/AdvancedExtender.java
+++ b/core/java/android/hardware/camera2/extension/AdvancedExtender.java
@@ -61,7 +61,6 @@
     private CameraUsageTracker mCameraUsageTracker;
     private static final String TAG = "AdvancedExtender";
 
-
     /**
      * Initialize a camera extension advanced extender instance.
      *
@@ -263,6 +262,13 @@
      *
      * <p>For example, an extension may limit the zoom ratio range. In this case, an OEM can return
      * a new zoom ratio range for the key {@link CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE}.
+     *
+     * <p> Currently, the only synthetic keys supported for override are
+     * {@link CameraCharacteristics#REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES} and
+     * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}. To enable them, an OEM
+     * should override the respective native keys
+     * {@link CameraCharacteristics#REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP} and
+     *  {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP}.
      */
     @FlaggedApi(Flags.FLAG_CAMERA_EXTENSIONS_CHARACTERISTICS_GET)
     @NonNull
diff --git a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
index 509bcb8..5567bed 100644
--- a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
+++ b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
@@ -27,6 +27,7 @@
     int imageFormat;
     int capacity;
     long usage;
+    long dynamicRangeProfile;
 
     const int TYPE_SURFACE = 0;
     const int TYPE_IMAGEREADER = 1;
diff --git a/core/java/android/hardware/camera2/extension/CameraOutputSurface.java b/core/java/android/hardware/camera2/extension/CameraOutputSurface.java
index 53f56bc..001b794 100644
--- a/core/java/android/hardware/camera2/extension/CameraOutputSurface.java
+++ b/core/java/android/hardware/camera2/extension/CameraOutputSurface.java
@@ -133,15 +133,4 @@
             @DynamicRangeProfiles.Profile long dynamicRangeProfile) {
         mOutputSurface.dynamicRangeProfile = dynamicRangeProfile;
     }
-
-    /**
-     * Set the color space. The default colorSpace
-     * will be
-     * {@link android.hardware.camera2.params.ColorSpaceProfiles.UNSPECIFIED}
-     * unless explicitly set using this method.
-     */
-    @FlaggedApi(Flags.FLAG_EXTENSION_10_BIT)
-    public void setColorSpace(int colorSpace) {
-        mOutputSurface.colorSpace = colorSpace;
-    }
 }
diff --git a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl
index 84ca2b6..c4f653c 100644
--- a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl
+++ b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl
@@ -25,4 +25,5 @@
     CameraMetadataNative sessionParameter;
     int sessionTemplateId;
     int sessionType;
+    int colorSpace;
 }
diff --git a/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java b/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java
index 96c88e6..84b7a7f 100644
--- a/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java
+++ b/core/java/android/hardware/camera2/extension/ExtensionConfiguration.java
@@ -22,6 +22,7 @@
 import android.annotation.SystemApi;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.ColorSpaceProfiles;
 import android.os.IBinder;
 
 import com.android.internal.camera.flags.Flags;
@@ -48,6 +49,7 @@
     private final int mSessionTemplateId;
     private final List<ExtensionOutputConfiguration> mOutputs;
     private final CaptureRequest mSessionParameters;
+    private int mColorSpace;
 
     /**
      * Initialize an extension configuration instance
@@ -72,6 +74,18 @@
         mSessionTemplateId = sessionTemplateId;
         mOutputs = outputs;
         mSessionParameters = sessionParams;
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
+    }
+
+    /**
+     * Set the color space using the ordinal value of a
+     * {@link android.graphics.ColorSpace.Named}.
+     * The default will be -1, indicating an unspecified ColorSpace,
+     * unless explicitly set using this method.
+     */
+    @FlaggedApi(Flags.FLAG_EXTENSION_10_BIT)
+    public void setColorSpace(int colorSpace) {
+        mColorSpace = colorSpace;
     }
 
     @FlaggedApi(Flags.FLAG_CONCERT_MODE)
@@ -84,6 +98,11 @@
         ret.sessionTemplateId = mSessionTemplateId;
         ret.sessionType = mSessionType;
         ret.outputConfigs = new ArrayList<>(mOutputs.size());
+        if (Flags.extension10Bit()) {
+            ret.colorSpace = mColorSpace;
+        } else {
+            ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED;
+        }
         for (ExtensionOutputConfiguration outputConfig : mOutputs) {
             ret.outputConfigs.add(outputConfig.getOutputConfig());
         }
diff --git a/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java b/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java
index 9dc6d7b..3a67d61 100644
--- a/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java
+++ b/core/java/android/hardware/camera2/extension/ExtensionOutputConfiguration.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.hardware.camera2.params.DynamicRangeProfiles;
 
 import com.android.internal.camera.flags.Flags;
 
@@ -79,6 +80,11 @@
         config.outputId = new OutputConfigId();
         config.outputId.id = mOutputConfigId;
         config.surfaceGroupId = mSurfaceGroupId;
+        if (Flags.extension10Bit()) {
+            config.dynamicRangeProfile = surface.getDynamicRangeProfile();
+        } else {
+            config.dynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+        }
     }
 
     @Nullable CameraOutputConfig getOutputConfig() {
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index a7d6caf..5b7f8bb 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.content.Context;
+import android.graphics.ColorSpace;
 import android.graphics.ImageFormat;
 import android.graphics.SurfaceTexture;
 import android.hardware.SyncFence;
@@ -49,6 +50,7 @@
 import android.hardware.camera2.extension.ParcelImage;
 import android.hardware.camera2.extension.ParcelTotalCaptureResult;
 import android.hardware.camera2.extension.Request;
+import android.hardware.camera2.params.ColorSpaceProfiles;
 import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.hardware.camera2.params.ExtensionSessionConfiguration;
 import android.hardware.camera2.params.OutputConfiguration;
@@ -62,6 +64,7 @@
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.Size;
 import android.view.Surface;
@@ -97,6 +100,9 @@
     private Surface mClientRepeatingRequestSurface;
     private Surface mClientCaptureSurface;
     private Surface mClientPostviewSurface;
+    private OutputConfiguration mClientRepeatingRequestOutputConfig;
+    private OutputConfiguration mClientCaptureOutputConfig;
+    private OutputConfiguration mClientPostviewOutputConfig;
     private CameraCaptureSession mCaptureSession = null;
     private ISessionProcessorImpl mSessionProcessor = null;
     private final InitializeSessionHandler mInitializeHandler;
@@ -142,8 +148,19 @@
 
         for (OutputConfiguration c : config.getOutputConfigurations()) {
             if (c.getDynamicRangeProfile() != DynamicRangeProfiles.STANDARD) {
-                throw new IllegalArgumentException("Unsupported dynamic range profile: " +
-                        c.getDynamicRangeProfile());
+                if (Flags.extension10Bit() && Flags.cameraExtensionsCharacteristicsGet()) {
+                    DynamicRangeProfiles dynamicProfiles = extensionChars.get(
+                            config.getExtension(),
+                            CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES);
+                    if (dynamicProfiles == null || !dynamicProfiles.getSupportedProfiles()
+                            .contains(c.getDynamicRangeProfile())) {
+                        throw new IllegalArgumentException("Unsupported dynamic range profile: "
+                                + c.getDynamicRangeProfile());
+                    }
+                } else {
+                    throw new IllegalArgumentException("Unsupported dynamic range profile: "
+                            + c.getDynamicRangeProfile());
+                }
             }
             if (c.getStreamUseCase() !=
                     CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT) {
@@ -157,12 +174,26 @@
                 config.getExtension(), SurfaceTexture.class);
         Surface repeatingRequestSurface = CameraExtensionUtils.getRepeatingRequestSurface(
                 config.getOutputConfigurations(), supportedPreviewSizes);
+        OutputConfiguration repeatingRequestOutputConfig = null;
         if (repeatingRequestSurface != null) {
+            for (OutputConfiguration outputConfig : config.getOutputConfigurations()) {
+                if (outputConfig.getSurface() == repeatingRequestSurface) {
+                    repeatingRequestOutputConfig = outputConfig;
+                }
+            }
             suitableSurfaceCount++;
         }
 
         HashMap<Integer, List<Size>> supportedCaptureSizes = new HashMap<>();
-        for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) {
+
+        IntArray supportedCaptureOutputFormats =
+                new IntArray(CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS.length);
+        supportedCaptureOutputFormats.addAll(
+                CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS);
+        if (Flags.extension10Bit()) {
+            supportedCaptureOutputFormats.add(ImageFormat.YCBCR_P010);
+        }
+        for (int format : supportedCaptureOutputFormats.toArray()) {
             List<Size> supportedSizes = extensionChars.getExtensionSupportedSizes(
                     config.getExtension(), format);
             if (supportedSizes != null) {
@@ -171,7 +202,13 @@
         }
         Surface burstCaptureSurface = CameraExtensionUtils.getBurstCaptureSurface(
                 config.getOutputConfigurations(), supportedCaptureSizes);
+        OutputConfiguration burstCaptureOutputConfig = null;
         if (burstCaptureSurface != null) {
+            for (OutputConfiguration outputConfig : config.getOutputConfigurations()) {
+                if (outputConfig.getSurface() == burstCaptureSurface) {
+                    burstCaptureOutputConfig = outputConfig;
+                }
+            }
             suitableSurfaceCount++;
         }
 
@@ -180,13 +217,14 @@
         }
 
         Surface postviewSurface = null;
+        OutputConfiguration postviewOutputConfig = config.getPostviewOutputConfiguration();
         if (burstCaptureSurface != null && config.getPostviewOutputConfiguration() != null) {
             CameraExtensionUtils.SurfaceInfo burstCaptureSurfaceInfo =
                     CameraExtensionUtils.querySurface(burstCaptureSurface);
             Size burstCaptureSurfaceSize =
                     new Size(burstCaptureSurfaceInfo.mWidth, burstCaptureSurfaceInfo.mHeight);
             HashMap<Integer, List<Size>> supportedPostviewSizes = new HashMap<>();
-            for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) {
+            for (int format : supportedCaptureOutputFormats.toArray()) {
                 List<Size> supportedSizesPostview = extensionChars.getPostviewSupportedSizes(
                         config.getExtension(), burstCaptureSurfaceSize, format);
                 if (supportedSizesPostview != null) {
@@ -207,8 +245,8 @@
         extender.init(cameraId, characteristicsMapNative);
 
         CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx,
-                extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface,
-                burstCaptureSurface, postviewSurface, config.getStateCallback(),
+                extender, cameraDevice, characteristicsMapNative, repeatingRequestOutputConfig,
+                burstCaptureOutputConfig, postviewOutputConfig, config.getStateCallback(),
                 config.getExecutor(), sessionId, token, config.getExtension());
 
         ret.mStatsAggregator.setClientName(ctx.getOpPackageName());
@@ -223,8 +261,9 @@
             @NonNull IAdvancedExtenderImpl extender,
             @NonNull CameraDeviceImpl cameraDevice,
             Map<String, CameraMetadataNative> characteristicsMap,
-            @Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface,
-            @Nullable Surface postviewSurface,
+            @Nullable OutputConfiguration repeatingRequestOutputConfig,
+            @Nullable OutputConfiguration burstCaptureOutputConfig,
+            @Nullable OutputConfiguration postviewOutputConfig,
             @NonNull StateCallback callback, @NonNull Executor executor,
             int sessionId,
             @NonNull IBinder token,
@@ -235,9 +274,18 @@
         mCharacteristicsMap = characteristicsMap;
         mCallbacks = callback;
         mExecutor = executor;
-        mClientRepeatingRequestSurface = repeatingRequestSurface;
-        mClientCaptureSurface = burstCaptureSurface;
-        mClientPostviewSurface = postviewSurface;
+        mClientRepeatingRequestOutputConfig = repeatingRequestOutputConfig;
+        mClientCaptureOutputConfig = burstCaptureOutputConfig;
+        mClientPostviewOutputConfig = postviewOutputConfig;
+        if (repeatingRequestOutputConfig != null) {
+            mClientRepeatingRequestSurface = repeatingRequestOutputConfig.getSurface();
+        }
+        if (burstCaptureOutputConfig != null) {
+            mClientCaptureSurface = burstCaptureOutputConfig.getSurface();
+        }
+        if (postviewOutputConfig != null) {
+            mClientPostviewSurface = postviewOutputConfig.getSurface();
+        }
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
@@ -262,9 +310,9 @@
             return;
         }
 
-        OutputSurface previewSurface = initializeParcelable(mClientRepeatingRequestSurface);
-        OutputSurface captureSurface = initializeParcelable(mClientCaptureSurface);
-        OutputSurface postviewSurface = initializeParcelable(mClientPostviewSurface);
+        OutputSurface previewSurface = initializeParcelable(mClientRepeatingRequestOutputConfig);
+        OutputSurface captureSurface = initializeParcelable(mClientCaptureOutputConfig);
+        OutputSurface postviewSurface = initializeParcelable(mClientPostviewOutputConfig);
 
         mSessionProcessor = mAdvancedExtender.getSessionProcessor();
         CameraSessionConfig sessionConfig = mSessionProcessor.initSession(mToken,
@@ -300,6 +348,7 @@
             cameraOutput.setTimestampBase(OutputConfiguration.TIMESTAMP_BASE_SENSOR);
             cameraOutput.setReadoutTimestampEnabled(false);
             cameraOutput.setPhysicalCameraId(output.physicalCameraId);
+            cameraOutput.setDynamicRangeProfile(output.dynamicRangeProfile);
             outputList.add(cameraOutput);
             mCameraConfigMap.put(cameraOutput.getSurface(), output);
         }
@@ -314,7 +363,10 @@
         SessionConfiguration sessionConfiguration = new SessionConfiguration(sessionType,
                 outputList, new CameraExtensionUtils.HandlerExecutor(mHandler),
                 new SessionStateHandler());
-
+        if (sessionConfig.colorSpace != ColorSpaceProfiles.UNSPECIFIED) {
+            sessionConfiguration.setColorSpace(
+                    ColorSpace.Named.values()[sessionConfig.colorSpace]);
+        }
         if ((sessionConfig.sessionParameter != null) &&
                 (!sessionConfig.sessionParameter.isEmpty())) {
             CaptureRequest.Builder requestBuilder = mCameraDevice.createCaptureRequest(
@@ -362,21 +414,38 @@
         return ret;
     }
 
-    private static OutputSurface initializeParcelable(Surface s) {
+    private static OutputSurface initializeParcelable(OutputConfiguration o) {
         OutputSurface ret = new OutputSurface();
-        if (s != null) {
+
+        if (o != null && o.getSurface() != null) {
+            Surface s = o.getSurface();
             ret.surface = s;
             ret.size = new android.hardware.camera2.extension.Size();
             Size surfaceSize = SurfaceUtils.getSurfaceSize(s);
             ret.size.width = surfaceSize.getWidth();
             ret.size.height = surfaceSize.getHeight();
             ret.imageFormat = SurfaceUtils.getSurfaceFormat(s);
+
+            if (Flags.extension10Bit()) {
+                ret.dynamicRangeProfile = o.getDynamicRangeProfile();
+                ColorSpace colorSpace = o.getColorSpace();
+                if (colorSpace != null) {
+                    ret.colorSpace = colorSpace.getId();
+                } else {
+                    ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED;
+                }
+            } else {
+                ret.dynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+                ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED;
+            }
         } else {
             ret.surface = null;
             ret.size = new android.hardware.camera2.extension.Size();
             ret.size.width = -1;
             ret.size.height = -1;
             ret.imageFormat = ImageFormat.UNKNOWN;
+            ret.dynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+            ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED;
         }
 
         return ret;
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 725b413..5b32f33 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -58,12 +58,15 @@
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.Pair;
 import android.util.Size;
 import android.view.Surface;
 
+import com.android.internal.camera.flags.Flags;
+
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -183,7 +186,14 @@
         }
 
         HashMap<Integer, List<Size>> supportedCaptureSizes = new HashMap<>();
-        for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) {
+        IntArray supportedCaptureOutputFormats =
+                new IntArray(CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS.length);
+        supportedCaptureOutputFormats.addAll(
+                CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS);
+        if (Flags.extension10Bit()) {
+            supportedCaptureOutputFormats.add(ImageFormat.YCBCR_P010);
+        }
+        for (int format : supportedCaptureOutputFormats.toArray()) {
             List<Size> supportedSizes = extensionChars.getExtensionSupportedSizes(
                     config.getExtension(), format);
             if (supportedSizes != null) {
@@ -207,7 +217,7 @@
             Size burstCaptureSurfaceSize =
                     new Size(burstCaptureSurfaceInfo.mWidth, burstCaptureSurfaceInfo.mHeight);
             HashMap<Integer, List<Size>> supportedPostviewSizes = new HashMap<>();
-            for (int format : CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS) {
+            for (int format : supportedCaptureOutputFormats.toArray()) {
                 List<Size> supportedSizesPostview = extensionChars.getPostviewSupportedSizes(
                         config.getExtension(), burstCaptureSurfaceSize, format);
                 if (supportedSizesPostview != null) {
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java
index a8066aa..f0c6e2e 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java
@@ -29,10 +29,13 @@
 import android.media.Image;
 import android.media.ImageWriter;
 import android.os.Handler;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.Size;
 import android.view.Surface;
 
+import com.android.internal.camera.flags.Flags;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -130,9 +133,16 @@
     public static Surface getBurstCaptureSurface(
             @NonNull List<OutputConfiguration> outputConfigs,
             @NonNull HashMap<Integer, List<Size>> supportedCaptureSizes) {
+        IntArray supportedCaptureOutputFormats =
+                new IntArray(CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS.length);
+        supportedCaptureOutputFormats.addAll(
+                CameraExtensionUtils.SUPPORTED_CAPTURE_OUTPUT_FORMATS);
+        if (Flags.extension10Bit()) {
+            supportedCaptureOutputFormats.add(ImageFormat.YCBCR_P010);
+        }
         for (OutputConfiguration config : outputConfigs) {
             SurfaceInfo surfaceInfo = querySurface(config.getSurface());
-            for (int supportedFormat : SUPPORTED_CAPTURE_OUTPUT_FORMATS) {
+            for (int supportedFormat : supportedCaptureOutputFormats.toArray()) {
                 if (surfaceInfo.mFormat == supportedFormat) {
                     Size captureSize = new Size(surfaceInfo.mWidth, surfaceInfo.mHeight);
                     if (supportedCaptureSizes.containsKey(supportedFormat)) {
diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
index 905d911..76888f3 100644
--- a/core/java/android/hardware/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -543,11 +543,13 @@
                 int identifier = source.readInt();
                 String name = source.readString8();
                 ArraySet<@DeviceStateProperties Integer> systemProperties = new ArraySet<>();
-                for (int i = 0; i < source.readInt(); i++) {
+                int systemPropertySize = source.readInt();
+                for (int i = 0; i < systemPropertySize; i++) {
                     systemProperties.add(source.readInt());
                 }
                 ArraySet<@DeviceStateProperties Integer> physicalProperties = new ArraySet<>();
-                for (int j = 0; j < source.readInt(); j++) {
+                int physicalPropertySize = source.readInt();
+                for (int j = 0; j < physicalPropertySize; j++) {
                     physicalProperties.add(source.readInt());
                 }
                 return new DeviceState.Configuration(identifier, name, systemProperties,
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index a3a2a2e..c5167db 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -304,7 +304,11 @@
      *
      * @param id primary identifier of a program to fetch
      * @return the program info, or null if there is no such program on the list
+     *
+     * @deprecated Use {@link #getProgramInfos(ProgramSelector.Identifier)} to get all programs
+     * with the given primary identifier
      */
+    @Deprecated
     public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
         Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries;
         synchronized (mLock) {
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index a968c6f..0740374 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -312,15 +312,23 @@
     public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
     /**
      * 1: AM, 2:FM
+     * @deprecated use {@link #IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
      */
+    @Deprecated
     public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11;
     /**
      * 32bit primary identifier for SiriusXM Satellite Radio.
+     *
+     * @deprecated SiriusXM Satellite Radio is not supported
      */
+    @Deprecated
     public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12;
     /**
      * 0-999 range
+     *
+     * @deprecated SiriusXM Satellite Radio is not supported
      */
+    @Deprecated
     public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13;
     /**
      * 44bit compound primary identifier for Digital Audio Broadcasting and
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 61cf8901..da6c686 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -166,7 +166,12 @@
      * analog handover state managed from the HAL implementation side.
      *
      * <p>Some radio technologies may not support this, i.e. DAB.
+     *
+     * @deprecated Use {@link #CONFIG_FORCE_ANALOG_FM} instead. If {@link #CONFIG_FORCE_ANALOG_FM}
+     * is supported in HAL, {@link RadioTuner#setConfigFlag} and {@link RadioTuner#isConfigFlagSet}
+     * with CONFIG_FORCE_ANALOG will set/get the value of {@link #CONFIG_FORCE_ANALOG_FM}.
      */
+    @Deprecated
     public static final int CONFIG_FORCE_ANALOG = 2;
     /**
      * Forces the digital playback for the supporting radio technology.
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index ce74848..82e613e 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -77,4 +77,12 @@
     description: "Properties to allow apps and activities to opt-in to cover display rendering"
     bug: "312530526"
     is_fixed_read_only: true
+}
+
+flag {
+    namespace: "windowing_sdk"
+    name: "enable_wm_extensions_for_all_flag"
+    description: "Whether to enable WM Extensions for all devices"
+    bug: "306666082"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/compat/Android.bp b/core/java/com/android/internal/compat/Android.bp
new file mode 100644
index 0000000..9ff05a6
--- /dev/null
+++ b/core/java/com/android/internal/compat/Android.bp
@@ -0,0 +1,7 @@
+aconfig_declarations {
+    name: "compat_logging_flags",
+    package: "com.android.internal.compat.flags",
+    srcs: [
+        "compat_logging_flags.aconfig",
+    ],
+}
diff --git a/core/java/com/android/internal/compat/ChangeReporter.java b/core/java/com/android/internal/compat/ChangeReporter.java
index b9d3df6..6ff546f 100644
--- a/core/java/com/android/internal/compat/ChangeReporter.java
+++ b/core/java/com/android/internal/compat/ChangeReporter.java
@@ -24,6 +24,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.compat.flags.Flags;
 import com.android.internal.util.FrameworkStatsLog;
 
 import java.lang.annotation.Retention;
@@ -40,7 +41,7 @@
  * @hide
  */
 public final class ChangeReporter {
-    private static final String TAG = "CompatibilityChangeReporter";
+    private static final String TAG = "CompatChangeReporter";
     private int mSource;
 
     private static final class ChangeReport {
@@ -84,19 +85,34 @@
      * Report the change to stats log and to the debug log if the change was not previously
      * logged already.
      *
+     * @param uid             affected by the change
+     * @param changeId        the reported change id
+     * @param state           of the reported change - enabled/disabled/only logged
+     * @param isLoggableBySdk whether debug logging is allowed for this change based on target
+     *                        SDK version. This is combined with other logic to determine whether to
+     *                        actually log. If the sdk version does not matter, should be true.
+     */
+    public void reportChange(int uid, long changeId, int state, boolean isLoggableBySdk) {
+        if (shouldWriteToStatsLog(uid, changeId, state)) {
+            FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid,
+                    changeId, state, mSource);
+        }
+        if (shouldWriteToDebug(uid, changeId, state, isLoggableBySdk)) {
+            debugLog(uid, changeId, state);
+        }
+        markAsReported(uid, new ChangeReport(changeId, state));
+    }
+
+    /**
+     * Report the change to stats log and to the debug log if the change was not previously
+     * logged already.
+     *
      * @param uid      affected by the change
      * @param changeId the reported change id
      * @param state    of the reported change - enabled/disabled/only logged
      */
     public void reportChange(int uid, long changeId, int state) {
-        if (shouldWriteToStatsLog(uid, changeId, state)) {
-            FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid,
-                    changeId, state, mSource);
-        }
-        if (shouldWriteToDebug(uid, changeId, state)) {
-            debugLog(uid, changeId, state);
-        }
-        markAsReported(uid, new ChangeReport(changeId, state));
+        reportChange(uid, changeId, state, true);
     }
 
     /**
@@ -130,14 +146,43 @@
     /**
      * Returns whether the next report should be logged to logcat.
      *
-     * @param uid      affected by the change
-     * @param changeId the reported change id
-     * @param state    of the reported change - enabled/disabled/only logged
+     * @param uid             affected by the change
+     * @param changeId        the reported change id
+     * @param state           of the reported change - enabled/disabled/only logged
+     * @param isLoggableBySdk whether debug logging is allowed for this change based on target
+     *                        SDK version. This is combined with other logic to determine whether to
+     *                        actually log. If the sdk version does not matter, should be true.
+     * @return true if the report should be logged
+     */
+    @VisibleForTesting
+    public boolean shouldWriteToDebug(
+            int uid, long changeId, int state, boolean isLoggableBySdk) {
+        // If log all bit is on, always return true.
+        if (mDebugLogAll) return true;
+        // If the change has already been reported, do not write.
+        if (isAlreadyReported(uid, new ChangeReport(changeId, state))) return false;
+
+        // If the flag is turned off or the TAG's logging is forced to debug level with
+        // `adb setprop log.tag.CompatChangeReporter=DEBUG`, write to debug since the above checks
+        // have already passed.
+        boolean skipLoggingFlag = Flags.skipOldAndDisabledCompatLogging();
+        if (!skipLoggingFlag || Log.isLoggable(TAG, Log.DEBUG)) return true;
+
+        // Log if the change is enabled and targets the latest sdk version.
+        return isLoggableBySdk && state != STATE_DISABLED;
+    }
+
+    /**
+     * Returns whether the next report should be logged to logcat.
+     *
+     * @param uid         affected by the change
+     * @param changeId    the reported change id
+     * @param state       of the reported change - enabled/disabled/only logged
      * @return true if the report should be logged
      */
     @VisibleForTesting
     public boolean shouldWriteToDebug(int uid, long changeId, int state) {
-        return mDebugLogAll || !isAlreadyReported(uid, new ChangeReport(changeId, state));
+        return shouldWriteToDebug(uid, changeId, state, true);
     }
 
     private boolean isAlreadyReported(int uid, ChangeReport report) {
diff --git a/core/java/com/android/internal/compat/compat_logging_flags.aconfig b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
new file mode 100644
index 0000000..fab3856
--- /dev/null
+++ b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.internal.compat.flags"
+
+flag {
+    name: "skip_old_and_disabled_compat_logging"
+    namespace: "platform_compat"
+    description: "Feature flag for skipping debug logging for changes that do not target the latest sdk or are disabled"
+    bug: "323949942"
+    is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index cbe0700..d4dcec9 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -93,6 +93,9 @@
             throw ex;
         }
 
+        if (peer.getUid() != Process.SYSTEM_UID) {
+            throw new ZygoteSecurityException("Only system UID is allowed to connect to Zygote.");
+        }
         isEof = false;
     }
 
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index dc3b5a8..0257033 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -33,8 +33,10 @@
 import com.android.internal.inputmethod.InputBindResult;
 
 /**
- * Public interface to the global input method manager, used by all client
- * applications.
+ * Public interface to the global input method manager, used by all client applications.
+ *
+ * When adding new methods, make sure the associated user can be inferred from the arguments.
+ * Consider passing the associated userId when not already passing a display id or a window token.
  */
 interface IInputMethodManager {
     void addClient(in IInputMethodClient client, in IRemoteInputConnection inputmethod,
diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
index 54c4cd5..e0cc055 100644
--- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
+++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
@@ -354,6 +354,18 @@
   return result;
 }
 
+static uid_t getSocketPeerUid(int socket, const std::function<void(const std::string&)>& fail_fn) {
+  struct ucred credentials;
+  socklen_t cred_size = sizeof credentials;
+  if (getsockopt(socket, SOL_SOCKET, SO_PEERCRED, &credentials, &cred_size) == -1
+      || cred_size != sizeof credentials) {
+    fail_fn(CREATE_ERROR("Failed to get socket credentials, %s",
+                         strerror(errno)));
+  }
+
+  return credentials.uid;
+}
+
 // Read all lines from the current command into the buffer, and then reset the buffer, so
 // we will start reading again at the beginning of the command, starting with the argument
 // count. And we don't need access to the fd to do so.
@@ -413,19 +425,12 @@
     fail_fn_z("Failed to retrieve session socket timeout");
   }
 
-  struct ucred credentials;
-  socklen_t cred_size = sizeof credentials;
-  if (getsockopt(n_buffer->getFd(), SOL_SOCKET, SO_PEERCRED, &credentials, &cred_size) == -1
-      || cred_size != sizeof credentials) {
-    fail_fn_1(CREATE_ERROR("ForkRepeatedly failed to get initial credentials, %s",
-                           strerror(errno)));
+  uid_t peerUid = getSocketPeerUid(session_socket, fail_fn_1);
+  if (peerUid != static_cast<uid_t>(expected_uid)) {
+    return JNI_FALSE;
   }
-
   bool first_time = true;
   do {
-    if (credentials.uid != static_cast<uid_t>(expected_uid)) {
-      return JNI_FALSE;
-    }
     n_buffer->readAllLines(first_time ? fail_fn_1 : fail_fn_n);
     n_buffer->reset();
     int pid = zygote::forkApp(env, /* no pipe FDs */ -1, -1, session_socket_fds,
@@ -453,6 +458,7 @@
       }
     }
     for (;;) {
+      bool valid_session_socket = true;
       // Clear buffer and get count from next command.
       n_buffer->clear();
       // Poll isn't strictly necessary for now. But without it, disconnect is hard to detect.
@@ -463,25 +469,50 @@
       if ((fd_structs[SESSION_IDX].revents & POLLIN) != 0) {
         if (n_buffer->getCount(fail_fn_z) != 0) {
           break;
-        }  // else disconnected;
+        } else {
+          // Session socket was disconnected
+          valid_session_socket = false;
+          close(session_socket);
+        }
       } else if (poll_res == 0 || (fd_structs[ZYGOTE_IDX].revents & POLLIN) == 0) {
         fail_fn_z(
             CREATE_ERROR("Poll returned with no descriptors ready! Poll returned %d", poll_res));
       }
-      // We've now seen either a disconnect or connect request.
-      close(session_socket);
-      int new_fd = TEMP_FAILURE_RETRY(accept(zygote_socket_fd, nullptr, nullptr));
+      int new_fd = -1;
+      do {
+        // We've now seen either a disconnect or connect request.
+        new_fd = TEMP_FAILURE_RETRY(accept(zygote_socket_fd, nullptr, nullptr));
+        if (new_fd == -1) {
+          fail_fn_z(CREATE_ERROR("Accept(%d) failed: %s", zygote_socket_fd, strerror(errno)));
+        }
+        uid_t newPeerUid = getSocketPeerUid(new_fd, fail_fn_1);
+        if (newPeerUid != static_cast<uid_t>(expected_uid)) {
+          ALOGW("Dropping new connection with a mismatched uid %d\n", newPeerUid);
+          close(new_fd);
+          new_fd = -1;
+        } else {
+          // If we still have a valid session socket, close it now
+          if (valid_session_socket) {
+              close(session_socket);
+          }
+          valid_session_socket = true;
+        }
+      } while (!valid_session_socket);
+
+      // At this point we either have a valid new connection (new_fd > 0), or
+      // an existing session socket we can poll on
       if (new_fd == -1) {
-        fail_fn_z(CREATE_ERROR("Accept(%d) failed: %s", zygote_socket_fd, strerror(errno)));
+        // The new connection wasn't valid, and we still have an old one; retry polling
+        continue;
       }
       if (new_fd != session_socket) {
-          // Move new_fd back to the old value, so that we don't have to change Java-level data
-          // structures to reflect a change. This implicitly closes the old one.
-          if (TEMP_FAILURE_RETRY(dup2(new_fd, session_socket)) != session_socket) {
-            fail_fn_z(CREATE_ERROR("Failed to move fd %d to %d: %s",
-                                   new_fd, session_socket, strerror(errno)));
-          }
-          close(new_fd);  //  On Linux, fd is closed even if EINTR is returned.
+        // Move new_fd back to the old value, so that we don't have to change Java-level data
+        // structures to reflect a change. This implicitly closes the old one.
+        if (TEMP_FAILURE_RETRY(dup2(new_fd, session_socket)) != session_socket) {
+          fail_fn_z(CREATE_ERROR("Failed to move fd %d to %d: %s",
+                                 new_fd, session_socket, strerror(errno)));
+        }
+        close(new_fd);  //  On Linux, fd is closed even if EINTR is returned.
       }
       // If we ever return, we effectively reuse the old Java ZygoteConnection.
       // None of its state needs to change.
@@ -493,13 +524,6 @@
         fail_fn_z(CREATE_ERROR("Failed to set send timeout for socket %d: %s",
                                session_socket, strerror(errno)));
       }
-      if (getsockopt(session_socket, SOL_SOCKET, SO_PEERCRED, &credentials, &cred_size) == -1) {
-        fail_fn_z(CREATE_ERROR("ForkMany failed to get credentials: %s", strerror(errno)));
-      }
-      if (cred_size != sizeof credentials) {
-        fail_fn_z(CREATE_ERROR("ForkMany credential size = %d, should be %d",
-                               cred_size, static_cast<int>(sizeof credentials)));
-      }
     }
     first_time = false;
   } while (n_buffer->isSimpleForkCommand(minUid, fail_fn_n));
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
index a034653..10ac05d 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
@@ -88,13 +88,6 @@
     }
 
     @Test
-    public void setInternalHalCallback_callbackSetInHal() throws Exception {
-        mRadioModule.setInternalHalCallback();
-
-        verify(mBroadcastRadioMock).setTunerCallback(any());
-    }
-
-    @Test
     public void getImage_withValidIdFromRadioModule() {
         int imageId = 1;
 
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
index 262f167..755bcdb 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
@@ -192,66 +192,6 @@
             mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
             return null;
         }).when(mBroadcastRadioMock).setTunerCallback(any());
-        mRadioModule.setInternalHalCallback();
-
-        doAnswer(invocation -> {
-            android.hardware.broadcastradio.ProgramSelector halSel =
-                    (android.hardware.broadcastradio.ProgramSelector) invocation.getArguments()[0];
-            mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY);
-            if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY_KHZ) {
-                throw new ServiceSpecificException(Result.NOT_SUPPORTED);
-            }
-            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
-            return Result.OK;
-        }).when(mBroadcastRadioMock).tune(any());
-
-        doAnswer(invocation -> {
-            if ((boolean) invocation.getArguments()[0]) {
-                mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING;
-            } else {
-                mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING;
-            }
-            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
-            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
-            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
-            return Result.OK;
-        }).when(mBroadcastRadioMock).step(anyBoolean());
-
-        doAnswer(invocation -> {
-            if (mHalCurrentInfo == null) {
-                android.hardware.broadcastradio.ProgramSelector placeHolderSelector =
-                        AidlTestUtils.makeHalFmSelector(/* freq= */ 97300);
-
-                mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector);
-                return Result.OK;
-            }
-            mHalCurrentInfo.selector.primaryId.value = getSeekFrequency(
-                    mHalCurrentInfo.selector.primaryId.value,
-                    !(boolean) invocation.getArguments()[0]);
-            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
-            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
-            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
-            return Result.OK;
-        }).when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean());
-
-        doReturn(null).when(mBroadcastRadioMock).getImage(anyInt());
-
-        doAnswer(invocation -> {
-            int configFlag = (int) invocation.getArguments()[0];
-            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
-                throw new ServiceSpecificException(Result.NOT_SUPPORTED);
-            }
-            return mHalConfigMap.getOrDefault(configFlag, false);
-        }).when(mBroadcastRadioMock).isConfigFlagSet(anyInt());
-
-        doAnswer(invocation -> {
-            int configFlag = (int) invocation.getArguments()[0];
-            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
-                throw new ServiceSpecificException(Result.NOT_SUPPORTED);
-            }
-            mHalConfigMap.put(configFlag, (boolean) invocation.getArguments()[1]);
-            return null;
-        }).when(mBroadcastRadioMock).setConfigFlag(anyInt(), anyBoolean());
     }
 
     @After
@@ -330,6 +270,7 @@
 
         expect.withMessage("Close state of broadcast radio service session")
                 .that(mTunerSessions[0].isClosed()).isTrue();
+        verify(mBroadcastRadioMock).unsetTunerCallback();
     }
 
     @Test
@@ -351,6 +292,7 @@
                         .that(mTunerSessions[index].isClosed()).isFalse();
             }
         }
+        verify(mBroadcastRadioMock, never()).unsetTunerCallback();
     }
 
     @Test
@@ -378,6 +320,7 @@
             expect.withMessage("Close state of broadcast radio service session of index %s", index)
                     .that(mTunerSessions[index].isClosed()).isTrue();
         }
+        verify(mBroadcastRadioMock).unsetTunerCallback();
     }
 
     @Test
@@ -1295,6 +1238,71 @@
             mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class);
             mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]);
         }
+        setupMockedHalTunerSession();
+    }
+
+    private void setupMockedHalTunerSession() throws Exception {
+        expect.withMessage("Registered HAL tuner callback").that(mHalTunerCallback)
+                .isNotNull();
+
+        doAnswer(invocation -> {
+            android.hardware.broadcastradio.ProgramSelector halSel =
+                    (android.hardware.broadcastradio.ProgramSelector) invocation.getArguments()[0];
+            mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY);
+            if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY_KHZ) {
+                throw new ServiceSpecificException(Result.NOT_SUPPORTED);
+            }
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mBroadcastRadioMock).tune(any());
+
+        doAnswer(invocation -> {
+            if ((boolean) invocation.getArguments()[0]) {
+                mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING;
+            } else {
+                mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING;
+            }
+            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mBroadcastRadioMock).step(anyBoolean());
+
+        doAnswer(invocation -> {
+            if (mHalCurrentInfo == null) {
+                android.hardware.broadcastradio.ProgramSelector placeHolderSelector =
+                        AidlTestUtils.makeHalFmSelector(/* freq= */ 97300);
+
+                mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector);
+                return Result.OK;
+            }
+            mHalCurrentInfo.selector.primaryId.value = getSeekFrequency(
+                    mHalCurrentInfo.selector.primaryId.value,
+                    !(boolean) invocation.getArguments()[0]);
+            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean());
+
+        doReturn(null).when(mBroadcastRadioMock).getImage(anyInt());
+
+        doAnswer(invocation -> {
+            int configFlag = (int) invocation.getArguments()[0];
+            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
+                throw new ServiceSpecificException(Result.NOT_SUPPORTED);
+            }
+            return mHalConfigMap.getOrDefault(configFlag, false);
+        }).when(mBroadcastRadioMock).isConfigFlagSet(anyInt());
+
+        doAnswer(invocation -> {
+            int configFlag = (int) invocation.getArguments()[0];
+            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
+                throw new ServiceSpecificException(Result.NOT_SUPPORTED);
+            }
+            mHalConfigMap.put(configFlag, (boolean) invocation.getArguments()[1]);
+            return null;
+        }).when(mBroadcastRadioMock).setConfigFlag(anyInt(), anyBoolean());
     }
 
     private long getSeekFrequency(long currentFrequency, boolean seekDown) {
diff --git a/core/tests/PlatformCompatFramework/Android.bp b/core/tests/PlatformCompatFramework/Android.bp
index 95e23ad..2621d28 100644
--- a/core/tests/PlatformCompatFramework/Android.bp
+++ b/core/tests/PlatformCompatFramework/Android.bp
@@ -18,6 +18,7 @@
     static_libs: [
         "junit",
         "androidx.test.rules",
+        "flag-junit",
     ],
     platform_apis: true,
 }
diff --git a/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java b/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java
index a052543..12a42f9 100644
--- a/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java
+++ b/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java
@@ -19,9 +19,17 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.internal.compat.flags.Flags;
+
+import org.junit.Rule;
 import org.junit.Test;
 
 public class ChangeReporterTest {
+
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Test
     public void testStatsLogOnce() {
         ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE);
@@ -63,7 +71,7 @@
         ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE);
         int myUid = 1022, otherUid = 1023;
         long myChangeId = 500L, otherChangeId = 600L;
-        int myState = ChangeReporter.STATE_ENABLED, otherState = ChangeReporter.STATE_DISABLED;
+        int myState = ChangeReporter.STATE_ENABLED, otherState = ChangeReporter.STATE_LOGGED;
 
         assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myState));
         reporter.reportChange(myUid, myChangeId, myState);
@@ -112,4 +120,80 @@
         reporter.stopDebugLogAll();
         assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myState));
     }
+
+    @Test
+    public void testDebugLogWithFlagOnAndOldSdk() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_SKIP_OLD_AND_DISABLED_COMPAT_LOGGING);
+        ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE);
+        int myUid = 1022;
+        long myChangeId = 500L;
+        int myEnabledState = ChangeReporter.STATE_ENABLED;
+        int myDisabledState = ChangeReporter.STATE_DISABLED;
+
+        // Report will not log if target sdk is before the previous version.
+        assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, false));
+
+        reporter.resetReportedChanges(myUid);
+
+        // Report will be logged if target sdk is the latest version.
+        assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, true));
+
+        reporter.resetReportedChanges(myUid);
+
+        // If the report is disabled, the sdk version shouldn't matter.
+        assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, true));
+    }
+
+    @Test
+    public void testDebugLogWithFlagOnAndDisabledChange() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_SKIP_OLD_AND_DISABLED_COMPAT_LOGGING);
+        ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE);
+        int myUid = 1022;
+        long myChangeId = 500L;
+        int myEnabledState = ChangeReporter.STATE_ENABLED;
+        int myDisabledState = ChangeReporter.STATE_DISABLED;
+
+        // Report will not log if the change is disabled.
+        assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, true));
+
+        reporter.resetReportedChanges(myUid);
+
+        // Report will be logged if the change is enabled.
+        assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, true));
+
+        reporter.resetReportedChanges(myUid);
+
+        // If the report is not the latest version, the disabled state doesn't matter.
+        assertFalse(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, false));
+    }
+
+    @Test
+    public void testDebugLogWithFlagOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_SKIP_OLD_AND_DISABLED_COMPAT_LOGGING);
+        ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE);
+        int myUid = 1022;
+        long myChangeId = 500L;
+        int myEnabledState = ChangeReporter.STATE_ENABLED;
+        int myDisabledState = ChangeReporter.STATE_DISABLED;
+
+        // Report will be logged even if the change is not the latest sdk but the flag is off.
+        assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, false));
+
+        reporter.resetReportedChanges(myUid);
+
+        // Report will be logged if the change is enabled and the latest sdk but the flag is off.
+        assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myEnabledState, true));
+
+        reporter.resetReportedChanges(myUid);
+
+        // Report will be logged if the change is disabled and the latest sdk but the flag is
+        // off.
+        assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, true));
+
+        reporter.resetReportedChanges(myUid);
+
+        // Report will be logged if the change is disabled and not the latest sdk but the flag is
+        // off.
+        assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, false));
+    }
 }
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java
index 76772b7..0897726 100644
--- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java
@@ -16,6 +16,12 @@
 
 package android.hardware.devicestate;
 
+import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN;
+import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotNull;
 
@@ -33,6 +39,7 @@
 import org.junit.runners.JUnit4;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * Unit tests for {@link DeviceStateInfo}.
@@ -44,11 +51,25 @@
 public final class DeviceStateInfoTest {
 
     private static final DeviceState DEVICE_STATE_0 = new DeviceState(
-            new DeviceState.Configuration.Builder(0, "STATE_0").build());
+            new DeviceState.Configuration.Builder(0, "STATE_0")
+                    .setSystemProperties(
+                            Set.of(PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS,
+                                    PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY))
+                    .setPhysicalProperties(
+                            Set.of(PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED))
+                    .build());
     private static final DeviceState DEVICE_STATE_1 = new DeviceState(
-            new DeviceState.Configuration.Builder(1, "STATE_1").build());
+            new DeviceState.Configuration.Builder(1, "STATE_1")
+                    .setSystemProperties(
+                            Set.of(PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY))
+                    .setPhysicalProperties(
+                            Set.of(PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN))
+                    .build());
     private static final DeviceState DEVICE_STATE_2 = new DeviceState(
-            new DeviceState.Configuration.Builder(2, "STATE_2").build());
+            new DeviceState.Configuration.Builder(2, "STATE_2")
+                    .setSystemProperties(
+                            Set.of(PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY))
+                    .build());
 
     @Test
     public void create() {
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
index 68de21f..78d4324 100644
--- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
@@ -93,4 +93,22 @@
 
         Assert.assertEquals(originalState, new DeviceState(stateConfiguration));
     }
+
+    @Test
+    public void writeToParcel_noPhysicalProperties() {
+        final DeviceState originalState = new DeviceState(
+                new DeviceState.Configuration.Builder(0, "TEST_STATE")
+                        .setSystemProperties(Set.of(PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS,
+                                PROPERTY_POLICY_AVAILABLE_FOR_APP_REQUEST))
+                        .build());
+
+        final Parcel parcel = Parcel.obtain();
+        originalState.getConfiguration().writeToParcel(parcel, 0 /* flags */);
+        parcel.setDataPosition(0);
+
+        final DeviceState.Configuration stateConfiguration =
+                DeviceState.Configuration.CREATOR.createFromParcel(parcel);
+
+        Assert.assertEquals(originalState, new DeviceState(stateConfiguration));
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 539832e..d44033c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -523,8 +523,8 @@
     /**
      * Whether we should use jump cut for the change transition.
      * This normally happens when opening a new secondary with the existing primary using a
-     * different split layout. This can be complicated, like from horizontal to vertical split with
-     * new split pairs.
+     * different split layout (ratio or direction). This can be complicated, like from horizontal to
+     * vertical split with new split pairs.
      * Uses a jump cut animation to simplify.
      */
     private boolean shouldUseJumpCutForChangeTransition(@NonNull TransitionInfo info) {
@@ -553,8 +553,8 @@
         }
 
         // Check if the transition contains both opening and closing windows.
-        boolean hasOpeningWindow = false;
-        boolean hasClosingWindow = false;
+        final List<TransitionInfo.Change> openChanges = new ArrayList<>();
+        final List<TransitionInfo.Change> closeChanges = new ArrayList<>();
         for (TransitionInfo.Change change : info.getChanges()) {
             if (changingChanges.contains(change)) {
                 continue;
@@ -564,10 +564,30 @@
                 // No-op if it will be covered by the changing parent window.
                 continue;
             }
-            hasOpeningWindow |= TransitionUtil.isOpeningType(change.getMode());
-            hasClosingWindow |= TransitionUtil.isClosingType(change.getMode());
+            if (TransitionUtil.isOpeningType(change.getMode())) {
+                openChanges.add(change);
+            } else if (TransitionUtil.isClosingType(change.getMode())) {
+                closeChanges.add(change);
+            }
         }
-        return hasOpeningWindow && hasClosingWindow;
+        if (openChanges.isEmpty() || closeChanges.isEmpty()) {
+            // Only skip if the transition contains both open and close.
+            return false;
+        }
+        if (changingChanges.size() != 1 || openChanges.size() != 1 || closeChanges.size() != 1) {
+            // Skip when there are too many windows involved.
+            return true;
+        }
+        final TransitionInfo.Change changingChange = changingChanges.get(0);
+        final TransitionInfo.Change openChange = openChanges.get(0);
+        final TransitionInfo.Change closeChange = closeChanges.get(0);
+        if (changingChange.getStartAbsBounds().equals(openChange.getEndAbsBounds())
+                && changingChange.getEndAbsBounds().equals(closeChange.getStartAbsBounds())) {
+            // Don't skip if the transition is a simple shifting without split direction or ratio
+            // change. For example, A|B -> B|C.
+            return false;
+        }
+        return true;
     }
 
     /** Updates the changes to end states in {@code startTransaction} for jump cut animation. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 8b2ec0a..8d489e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -846,8 +846,10 @@
     static ShellController provideShellController(Context context,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
+            DisplayInsetsController displayInsetsController,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new ShellController(context, shellInit, shellCommandHandler, mainExecutor);
+        return new ShellController(context, shellInit, shellCommandHandler,
+                displayInsetsController, mainExecutor);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index fb3c35b..04f0f44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -57,6 +57,8 @@
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
 import com.android.wm.shell.dagger.pip.PipModule;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
+import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -509,6 +511,7 @@
             ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
             DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
             @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+            DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
             LaunchAdjacentController launchAdjacentController,
             RecentsTransitionHandler recentsTransitionHandler,
             MultiInstanceHelper multiInstanceHelper,
@@ -518,7 +521,8 @@
                 displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
                 dragAndDropController, transitions, enterDesktopTransitionHandler,
                 exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler,
-                dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController,
+                dragToDesktopTransitionHandler, desktopModeTaskRepository,
+                desktopModeLoggerTransitionObserver, launchAdjacentController,
                 recentsTransitionHandler, multiInstanceHelper, mainExecutor);
     }
 
@@ -562,6 +566,22 @@
         return new DesktopModeTaskRepository();
     }
 
+    @WMSingleton
+    @Provides
+    static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver(
+            ShellInit shellInit,
+            Transitions transitions,
+            DesktopModeEventLogger desktopModeEventLogger) {
+        return new DesktopModeLoggerTransitionObserver(
+                shellInit, transitions, desktopModeEventLogger);
+    }
+
+    @WMSingleton
+    @Provides
+    static DesktopModeEventLogger provideDesktopModeEventLogger() {
+        return new DesktopModeEventLogger();
+    }
+
     //
     // Drag and drop
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
new file mode 100644
index 0000000..a10c7c0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.TaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.os.IBinder
+import android.util.SparseArray
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.containsKey
+import androidx.core.util.forEach
+import androidx.core.util.isEmpty
+import androidx.core.util.isNotEmpty
+import androidx.core.util.plus
+import androidx.core.util.putAll
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.KtProtoLog
+
+/**
+ * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log
+ * appropriate desktop mode session log events. This observes transitions related to desktop mode
+ * and other transitions that originate both within and outside shell.
+ */
+class DesktopModeLoggerTransitionObserver(
+    shellInit: ShellInit,
+    private val transitions: Transitions,
+    private val desktopModeEventLogger: DesktopModeEventLogger
+) : Transitions.TransitionObserver {
+
+    private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) }
+
+    init {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) {
+            shellInit.addInitCallback(this::onInit, this)
+        }
+    }
+
+    // A sparse array of visible freeform tasks and taskInfos
+    private val visibleFreeformTaskInfos: SparseArray<TaskInfo> = SparseArray()
+
+    // Caching the taskInfos to handle canceled recents animations, if we identify that the recents
+    // animation was cancelled, we restore these tasks to calculate the post-Transition state
+    private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray()
+
+    // The instanceId for the current logging session
+    private var loggerInstanceId: InstanceId? = null
+
+    private val isSessionActive: Boolean
+        get() = loggerInstanceId != null
+
+    private fun setSessionInactive() {
+        loggerInstanceId = null
+    }
+
+    fun onInit() {
+        transitions.registerObserver(this)
+    }
+
+    override fun onTransitionReady(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction
+    ) {
+        // this was a new recents animation
+        if (info.isRecentsTransition() && tasksSavedForRecents.isEmpty()) {
+            KtProtoLog.v(
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopModeLogger: Recents animation running, saving tasks for later"
+            )
+            // TODO (b/326391303) - avoid logging session exit if we can identify a cancelled
+            // recents animation
+
+            // when recents animation is running, all freeform tasks are sent TO_BACK temporarily
+            // if the user ends up at home, we need to update the visible freeform tasks
+            // if the user cancels the animation, the subsequent transition is NONE
+            // if the user opens a new task, the subsequent transition is OPEN with flag
+            tasksSavedForRecents.putAll(visibleFreeformTaskInfos)
+        }
+
+        // figure out what the new state of freeform tasks would be post transition
+        var postTransitionVisibleFreeformTasks = getPostTransitionVisibleFreeformTaskInfos(info)
+
+        // A canceled recents animation is followed by a TRANSIT_NONE transition with no flags, if
+        // that's the case, we might have accidentally logged a session exit and would need to
+        // revaluate again. Add all the tasks back.
+        // This will start a new desktop mode session.
+        if (
+            info.type == WindowManager.TRANSIT_NONE &&
+                info.flags == 0 &&
+                tasksSavedForRecents.isNotEmpty()
+        ) {
+            KtProtoLog.v(
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopModeLogger: Canceled recents animation, restoring tasks"
+            )
+            // restore saved tasks in the updated set and clear for next use
+            postTransitionVisibleFreeformTasks += tasksSavedForRecents
+            tasksSavedForRecents.clear()
+        }
+
+        // identify if we need to log any changes and update the state of visible freeform tasks
+        identifyLogEventAndUpdateState(
+            transitionInfo = info,
+            preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos,
+            postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks
+        )
+    }
+
+    override fun onTransitionStarting(transition: IBinder) {}
+
+    override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
+
+    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {}
+
+    private fun getPostTransitionVisibleFreeformTaskInfos(
+        info: TransitionInfo
+    ): SparseArray<TaskInfo> {
+        // device is sleeping, so no task will be visible anymore
+        if (info.type == WindowManager.TRANSIT_SLEEP) {
+            return SparseArray()
+        }
+
+        // filter changes involving freeform tasks or tasks that were cached in previous state
+        val changesToFreeformWindows =
+            info.changes
+                .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID }
+                .filter {
+                    it.requireTaskInfo().isFreeformWindow() ||
+                        visibleFreeformTaskInfos.containsKey(it.requireTaskInfo().taskId)
+                }
+
+        val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray()
+        // start off by adding all existing tasks
+        postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos)
+
+        // the combined set of taskInfos we are interested in this transition change
+        for (change in changesToFreeformWindows) {
+            val taskInfo = change.requireTaskInfo()
+
+            // check if this task existed as freeform window in previous cached state and it's now
+            // changing window modes
+            if (
+                visibleFreeformTaskInfos.containsKey(taskInfo.taskId) &&
+                    visibleFreeformTaskInfos.get(taskInfo.taskId).isFreeformWindow() &&
+                    !taskInfo.isFreeformWindow()
+            ) {
+                postTransitionFreeformTasks.remove(taskInfo.taskId)
+                // no need to evaluate new visibility of this task, since it's no longer a freeform
+                // window
+                continue
+            }
+
+            // check if the task is visible after this change, otherwise remove it
+            if (isTaskVisibleAfterChange(change)) {
+                postTransitionFreeformTasks.put(taskInfo.taskId, taskInfo)
+            } else {
+                postTransitionFreeformTasks.remove(taskInfo.taskId)
+            }
+        }
+
+        KtProtoLog.v(
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopModeLogger: taskInfo map after processing changes %s",
+            postTransitionFreeformTasks.size()
+        )
+
+        return postTransitionFreeformTasks
+    }
+
+    /**
+     * Look at the [TransitionInfo.Change] and figure out if this task will be visible after this
+     * change is processed
+     */
+    private fun isTaskVisibleAfterChange(change: TransitionInfo.Change): Boolean =
+        when {
+            TransitionUtil.isOpeningType(change.mode) -> true
+            TransitionUtil.isClosingType(change.mode) -> false
+            // change mode TRANSIT_CHANGE is only for visible to visible transitions
+            change.mode == WindowManager.TRANSIT_CHANGE -> true
+            else -> false
+        }
+
+    /**
+     * Log the appropriate log event based on the new state of TasksInfos and previously cached
+     * state and update it
+     */
+    private fun identifyLogEventAndUpdateState(
+        transitionInfo: TransitionInfo,
+        preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
+        postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>
+    ) {
+        if (
+            postTransitionVisibleFreeformTasks.isEmpty() &&
+                preTransitionVisibleFreeformTasks.isNotEmpty() &&
+                isSessionActive
+        ) {
+            // Sessions is finishing, log task updates followed by an exit event
+            identifyAndLogTaskUpdates(
+                loggerInstanceId!!.id,
+                preTransitionVisibleFreeformTasks,
+                postTransitionVisibleFreeformTasks
+            )
+
+            desktopModeEventLogger.logSessionExit(
+                loggerInstanceId!!.id,
+                getExitReason(transitionInfo)
+            )
+
+            setSessionInactive()
+        } else if (
+            postTransitionVisibleFreeformTasks.isNotEmpty() &&
+                preTransitionVisibleFreeformTasks.isEmpty() &&
+                !isSessionActive
+        ) {
+            // Session is starting, log enter event followed by task updates
+            loggerInstanceId = idSequence.newInstanceId()
+            desktopModeEventLogger.logSessionEnter(
+                loggerInstanceId!!.id,
+                getEnterReason(transitionInfo)
+            )
+
+            identifyAndLogTaskUpdates(
+                loggerInstanceId!!.id,
+                preTransitionVisibleFreeformTasks,
+                postTransitionVisibleFreeformTasks
+            )
+        } else if (isSessionActive) {
+            // Session is neither starting, nor finishing, log task updates if there are any
+            identifyAndLogTaskUpdates(
+                loggerInstanceId!!.id,
+                preTransitionVisibleFreeformTasks,
+                postTransitionVisibleFreeformTasks
+            )
+        }
+
+        // update the state to the new version
+        visibleFreeformTaskInfos.clear()
+        visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks)
+    }
+
+    // TODO(b/326231724) - Add logging around taskInfoChanges Updates
+    /** Compare the old and new state of taskInfos and identify and log the changes */
+    private fun identifyAndLogTaskUpdates(
+        sessionId: Int,
+        preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
+        postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>
+    ) {
+        // find new tasks that were added
+        postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
+            if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) {
+                desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo))
+            }
+        }
+
+        // find old tasks that were removed
+        preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
+            if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) {
+                desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo))
+            }
+        }
+    }
+
+    // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo
+    private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate {
+        val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId)
+        // add task x, y if available
+        taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) }
+
+        return taskUpdate
+    }
+
+    /** Get [EnterReason] for this session enter */
+    private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason {
+        // TODO(b/326231756) - Add support for missing enter reasons
+        return when (transitionInfo.type) {
+            WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON
+            Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG
+            Transitions.TRANSIT_MOVE_TO_DESKTOP -> EnterReason.APP_HANDLE_MENU_BUTTON
+            WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT
+            else -> EnterReason.UNKNOWN_ENTER
+        }
+    }
+
+    /** Get [ExitReason] for this session exit */
+    private fun getExitReason(transitionInfo: TransitionInfo): ExitReason {
+        // TODO(b/326231756) - Add support for missing exit reasons
+        return when {
+            transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF
+            transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
+            transitionInfo.type == Transitions.TRANSIT_EXIT_DESKTOP_MODE -> ExitReason.DRAG_TO_EXIT
+            transitionInfo.isRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW
+            else -> ExitReason.UNKNOWN_EXIT
+        }
+    }
+
+    /** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */
+    @VisibleForTesting
+    fun addTaskInfosToCachedMap(taskInfo: TaskInfo) {
+        visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo)
+    }
+
+    @VisibleForTesting fun getLoggerSessionId(): Int? = loggerInstanceId?.id
+
+    @VisibleForTesting
+    fun setLoggerSessionId(id: Int) {
+        loggerInstanceId = InstanceId.fakeInstanceId(id)
+    }
+
+    private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo {
+        return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
+    }
+
+    private fun TaskInfo.isFreeformWindow(): Boolean {
+        return this.windowingMode == WINDOWING_MODE_FREEFORM
+    }
+
+    private fun TransitionInfo.isRecentsTransition(): Boolean {
+        return this.type == WindowManager.TRANSIT_TO_FRONT &&
+            this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
+    }
+}
\ No newline at end of file
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 95237c3..dd8c1a0 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
@@ -102,6 +102,7 @@
         ToggleResizeDesktopTaskTransitionHandler,
         private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
         private val desktopModeTaskRepository: DesktopModeTaskRepository,
+        private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
         private val launchAdjacentController: LaunchAdjacentController,
         private val recentsTransitionHandler: RecentsTransitionHandler,
         private val multiInstanceHelper: MultiInstanceHelper,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 9dd4c19..ec907fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -29,6 +29,7 @@
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -2776,7 +2777,7 @@
                                 + " with " + taskInfo.taskId + " before startAnimation().");
                         record.addRecord(stage, true, taskInfo.taskId);
                     }
-                } else if (isClosingType(change.getMode())) {
+                } else if (change.getMode() == TRANSIT_CLOSE) {
                     if (stage.containsTask(taskInfo.taskId)) {
                         record.addRecord(stage, false, taskInfo.taskId);
                         Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java
new file mode 100644
index 0000000..a94f802
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.sysui;
+
+import android.graphics.Rect;
+
+/**
+ * Callbacks for when the Display IME changes.
+ */
+public interface DisplayImeChangeListener {
+    /**
+     * Called when the ime bounds change.
+     */
+    default void onImeBoundsChanged(int displayId, Rect bounds) {}
+
+    /**
+     * Called when the IME visibility change.
+     */
+    default void onImeVisibilityChanged(int displayId, boolean isShowing) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index a7843e2..2f6edc2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -30,21 +30,28 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.util.ArrayMap;
+import android.view.InsetsSource;
+import android.view.InsetsState;
 import android.view.SurfaceControlRegistry;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ExternalThread;
 
 import java.io.PrintWriter;
 import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
 /**
@@ -57,6 +64,7 @@
     private final ShellInit mShellInit;
     private final ShellCommandHandler mShellCommandHandler;
     private final ShellExecutor mMainExecutor;
+    private final DisplayInsetsController mDisplayInsetsController;
     private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl();
 
     private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners =
@@ -65,6 +73,8 @@
             new CopyOnWriteArrayList<>();
     private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
             new CopyOnWriteArrayList<>();
+    private final ConcurrentHashMap<DisplayImeChangeListener, Executor> mDisplayImeChangeListeners =
+            new ConcurrentHashMap<>();
 
     private ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers =
             new ArrayMap<>();
@@ -73,20 +83,53 @@
 
     private Configuration mLastConfiguration;
 
+    private OnInsetsChangedListener mInsetsChangeListener = new OnInsetsChangedListener() {
+        private InsetsState mInsetsState = new InsetsState();
+
+        @Override
+        public void insetsChanged(InsetsState insetsState) {
+            if (mInsetsState == insetsState) {
+                return;
+            }
+
+            InsetsSource oldSource = mInsetsState.peekSource(InsetsSource.ID_IME);
+            boolean wasVisible = (oldSource != null && oldSource.isVisible());
+            Rect oldFrame = wasVisible ? oldSource.getFrame() : null;
+
+            InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME);
+            boolean isVisible = (newSource != null && newSource.isVisible());
+            Rect newFrame = isVisible ? newSource.getFrame() : null;
+
+            if (wasVisible != isVisible) {
+                onImeVisibilityChanged(isVisible);
+            }
+
+            if (newFrame != null && !newFrame.equals(oldFrame)) {
+                onImeBoundsChanged(newFrame);
+            }
+
+            mInsetsState = insetsState;
+        }
+    };
+
 
     public ShellController(Context context,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
+            DisplayInsetsController displayInsetsController,
             ShellExecutor mainExecutor) {
         mContext = context;
         mShellInit = shellInit;
         mShellCommandHandler = shellCommandHandler;
+        mDisplayInsetsController = displayInsetsController;
         mMainExecutor = mainExecutor;
         shellInit.addInitCallback(this::onInit, this);
     }
 
     private void onInit() {
         mShellCommandHandler.addDumpCallback(this::dump, this);
+        mDisplayInsetsController.addInsetsChangedListener(
+                mContext.getDisplayId(), mInsetsChangeListener);
     }
 
     /**
@@ -259,6 +302,25 @@
         }
     }
 
+    @VisibleForTesting
+    void onImeBoundsChanged(Rect bounds) {
+        ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime bounds changed");
+        mDisplayImeChangeListeners.forEach(
+                (DisplayImeChangeListener listener, Executor executor) ->
+                executor.execute(() -> listener.onImeBoundsChanged(
+                    mContext.getDisplayId(), bounds)));
+    }
+
+    @VisibleForTesting
+    void onImeVisibilityChanged(boolean isShowing) {
+        ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime visibility changed: isShowing=%b",
+                isShowing);
+        mDisplayImeChangeListeners.forEach(
+                (DisplayImeChangeListener listener, Executor executor) ->
+                executor.execute(() -> listener.onImeVisibilityChanged(
+                    mContext.getDisplayId(), isShowing)));
+    }
+
     private void handleInit() {
         SurfaceControlRegistry.createProcessInstance(mContext);
         mShellInit.init();
@@ -329,6 +391,19 @@
         }
 
         @Override
+        public void addDisplayImeChangeListener(DisplayImeChangeListener listener,
+                Executor executor) {
+            ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Adding new DisplayImeChangeListener");
+            mDisplayImeChangeListeners.put(listener, executor);
+        }
+
+        @Override
+        public void removeDisplayImeChangeListener(DisplayImeChangeListener listener) {
+            ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Removing DisplayImeChangeListener");
+            mDisplayImeChangeListeners.remove(listener);
+        }
+
+        @Override
         public boolean handleCommand(String[] args, PrintWriter pw) {
             try {
                 boolean[] result = new boolean[1];
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
index bc5dd11..bd1c64a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
@@ -25,6 +25,7 @@
 
 import java.io.PrintWriter;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * General interface for notifying the Shell of common SysUI events like configuration or keyguard
@@ -65,6 +66,18 @@
     default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
 
     /**
+     * Registers a DisplayImeChangeListener to monitor for changes on Ime
+     * position and visibility.
+     */
+    default void addDisplayImeChangeListener(DisplayImeChangeListener listener,
+            Executor executor) {}
+
+    /**
+     * Removes a registered DisplayImeChangeListener.
+     */
+    default void removeDisplayImeChangeListener(DisplayImeChangeListener listener) {}
+
+    /**
      * Handles a shell command.
      */
     default boolean handleCommand(final String[] args, PrintWriter pw) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index ae39fbc..4a4c5e8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -37,6 +37,7 @@
 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView
 import com.android.wm.shell.bubbles.properties.BubbleProperties
 import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayInsetsController
 import com.android.wm.shell.common.FloatingContentCoordinator
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
@@ -94,7 +95,8 @@
         val windowManager = context.getSystemService(WindowManager::class.java)
         val shellInit = ShellInit(mainExecutor)
         val shellCommandHandler = ShellCommandHandler()
-        val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor)
+        val shellController = ShellController(context, shellInit, shellCommandHandler,
+					      mock<DisplayInsetsController>(), mainExecutor)
         bubblePositioner = BubblePositioner(context, windowManager)
         val bubbleData =
             BubbleData(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
new file mode 100644
index 0000000..65117f7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS
+import android.view.WindowManager.TRANSIT_NONE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_SLEEP
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.view.WindowManager.TRANSIT_WAKE
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.TransitionInfoBuilder
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.times
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopModeLoggerTransitionObserverTest {
+
+    @JvmField
+    @Rule
+    val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
+            .mockStatic(DesktopModeEventLogger::class.java)
+            .mockStatic(DesktopModeStatus::class.java).build()!!
+
+    @Mock
+    lateinit var testExecutor: ShellExecutor
+    @Mock
+    private lateinit var mockShellInit: ShellInit
+    @Mock
+    private lateinit var transitions: Transitions
+
+    private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
+    private lateinit var shellInit: ShellInit
+    private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+
+    @Before
+    fun setup() {
+        Mockito.`when`(DesktopModeStatus.isEnabled()).thenReturn(true)
+        shellInit = Mockito.spy(ShellInit(testExecutor))
+        desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
+
+        transitionObserver = DesktopModeLoggerTransitionObserver(
+            mockShellInit, transitions, desktopModeEventLogger)
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            val initRunnableCaptor = ArgumentCaptor.forClass(
+                Runnable::class.java)
+            verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(),
+                same(transitionObserver))
+            initRunnableCaptor.value.run()
+        } else {
+            transitionObserver.onInit()
+        }
+    }
+
+    @Test
+    fun testRegistersObserverAtInit() {
+        verify(transitions)
+                .registerObserver(same(
+                    transitionObserver))
+    }
+
+    @Test
+    fun taskCreated_notFreeformWindow_doesNotLogSessionEnterOrTaskAdded() {
+        val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+        verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
+    }
+
+    @Test
+    fun taskCreated_FreeformWindowOpen_logSessionEnterAndTaskAdded() {
+        val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+        val sessionId = transitionObserver.getLoggerSessionId()
+
+        assertThat(sessionId).isNotNull()
+        verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+            eq(EnterReason.APP_FREEFORM_INTENT))
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    }
+
+    @Test
+    fun taskChanged_taskMovedToDesktopByDrag_logSessionEnterAndTaskAdded() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        // task change is finalised when drag ends
+        val transitionInfo = TransitionInfoBuilder(
+            Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+        val sessionId = transitionObserver.getLoggerSessionId()
+
+        assertThat(sessionId).isNotNull()
+        verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+            eq(EnterReason.APP_HANDLE_DRAG))
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    }
+
+    @Test
+    fun taskChanged_taskMovedToDesktopByButtonTap_logSessionEnterAndTaskAdded() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
+                .addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+        val sessionId = transitionObserver.getLoggerSessionId()
+
+        assertThat(sessionId).isNotNull()
+        verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+            eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    }
+
+    @Test
+    fun taskChanged_existingFreeformTaskMadeVisible_logSessionEnterAndTaskAdded() {
+        val taskInfo = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+        taskInfo.isVisibleRequested = true
+        val change = createChange(TRANSIT_CHANGE, taskInfo)
+        val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
+                .addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+        val sessionId = transitionObserver.getLoggerSessionId()
+
+        assertThat(sessionId).isNotNull()
+        verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+            eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    }
+
+    @Test
+    fun taskToFront_screenWake_logSessionStartedAndTaskAdded() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0)
+                .addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+        val sessionId = transitionObserver.getLoggerSessionId()
+
+        assertThat(sessionId).isNotNull()
+        verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+            eq(EnterReason.SCREEN_ON))
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    }
+
+    @Test
+    fun freeformTaskVisible_screenTurnOff_logSessionExitAndTaskRemoved_sessionIdNull() {
+        val sessionId = 1
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        transitionObserver.setLoggerSessionId(sessionId)
+
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+        verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+            eq(ExitReason.SCREEN_OFF))
+        assertThat(transitionObserver.getLoggerSessionId()).isNull()
+    }
+
+    @Test
+    fun freeformTaskVisible_exitDesktopUsingDrag_logSessionExitAndTaskRemoved_sessionIdNull() {
+        val sessionId = 1
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        transitionObserver.setLoggerSessionId(sessionId)
+
+        // window mode changing from FREEFORM to FULLSCREEN
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_EXIT_DESKTOP_MODE)
+                .addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+        verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+            eq(ExitReason.DRAG_TO_EXIT))
+        assertThat(transitionObserver.getLoggerSessionId()).isNull()
+    }
+
+    @Test
+    fun freeformTaskVisible_exitDesktopBySwipeUp_logSessionExitAndTaskRemoved_sessionIdNull() {
+        val sessionId = 1
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        transitionObserver.setLoggerSessionId(sessionId)
+
+        // recents transition
+        val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+                .addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+        verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+            eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+        assertThat(transitionObserver.getLoggerSessionId()).isNull()
+    }
+
+    @Test
+    fun freeformTaskVisible_taskFinished_logSessionExitAndTaskRemoved_sessionIdNull() {
+        val sessionId = 1
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        transitionObserver.setLoggerSessionId(sessionId)
+
+        // task closing
+        val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+        verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+            eq(ExitReason.TASK_FINISHED))
+        assertThat(transitionObserver.getLoggerSessionId()).isNull()
+    }
+
+    @Test
+    fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
+        val sessionId = 1
+        // add a freeform task to an existing session
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        transitionObserver.setLoggerSessionId(sessionId)
+
+        // recents transition sent freeform window to back
+        val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        val transitionInfo1 =
+            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change)
+                    .build()
+        callOnTransitionReady(transitionInfo1)
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+        verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+            eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+        assertThat(transitionObserver.getLoggerSessionId()).isNull()
+
+        val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
+        callOnTransitionReady(transitionInfo2)
+
+        verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
+    }
+
+    @Test
+    fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
+        val sessionId = 1
+        // add an existing freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        transitionObserver.setLoggerSessionId(sessionId)
+
+        // new freeform task added
+        val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+        verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+    }
+
+    @Test
+    fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
+        val sessionId = 1
+        // add two existing freeform tasks
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+        transitionObserver.setLoggerSessionId(sessionId)
+
+        // new freeform task added
+        val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+        verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
+    }
+
+    /**
+     * Simulate calling the onTransitionReady() method
+     */
+    private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+        val transition = mock(IBinder::class.java)
+        val startT = mock(
+            SurfaceControl.Transaction::class.java)
+        val finishT = mock(
+            SurfaceControl.Transaction::class.java)
+
+        transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
+    }
+
+    companion object {
+        fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
+            val taskInfo = ActivityManager.RunningTaskInfo()
+            taskInfo.taskId = taskId
+            taskInfo.configuration.windowConfiguration.windowingMode = windowMode
+
+            return taskInfo
+        }
+
+        fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
+            val change = Change(
+                WindowContainerToken(mock(
+                    IWindowContainerToken::class.java)),
+                mock(SurfaceControl::class.java))
+            change.mode = mode
+            change.taskInfo = taskInfo
+            return change
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 0136751..5df9dd3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -87,6 +87,7 @@
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.times
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.quality.Strictness
 
@@ -113,6 +114,7 @@
     @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
     @Mock lateinit var dragAndDropController: DragAndDropController
     @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
+    @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
 
     private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var controller: DesktopTasksController
@@ -163,6 +165,7 @@
             mToggleResizeDesktopTaskTransitionHandler,
             dragToDesktopTransitionHandler,
             desktopModeTaskRepository,
+            desktopModeLoggerTransitionObserver,
             launchAdjacentController,
             recentsTransitionHandler,
             multiInstanceHelper,
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 3384509..d38fc6c 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
@@ -129,7 +129,7 @@
         }).when(mMockExecutor).execute(any());
         mShellInit = spy(new ShellInit(mMockExecutor));
         mShellController = spy(new ShellController(mContext, mShellInit, mMockShellCommandHandler,
-                mMockExecutor));
+                mMockDisplayInsetsController, mMockExecutor));
         mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
                 mShellController, mMockDisplayController, mMockPipAnimationController,
                 mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 10e9e11..41a4e8d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -58,6 +58,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
@@ -96,6 +97,8 @@
     private DesktopModeTaskRepository mDesktopModeTaskRepository;
     @Mock
     private ActivityTaskManager mActivityTaskManager;
+    @Mock
+    private DisplayInsetsController mDisplayInsetsController;
 
     private ShellTaskOrganizer mShellTaskOrganizer;
     private RecentTasksController mRecentTasksController;
@@ -110,7 +113,7 @@
         when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
         mShellInit = spy(new ShellInit(mMainExecutor));
         mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
-                mMainExecutor));
+                mDisplayInsetsController, mMainExecutor));
         mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
                 mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
                 Optional.of(mDesktopModeTaskRepository), mMainExecutor);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 315d97e..3c387f0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -123,7 +123,7 @@
         assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
         MockitoAnnotations.initMocks(this);
         mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
-                mMainExecutor));
+                mDisplayInsetsController, mMainExecutor));
         mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
                 mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
                 mRootTDAOrganizer, mDisplayController, mDisplayImeController,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
index 012c4081..ff76a2f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
@@ -40,6 +40,7 @@
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -65,6 +66,7 @@
 
     private @Mock Context mContext;
     private @Mock DisplayManager mDisplayManager;
+    private @Mock DisplayInsetsController mDisplayInsetsController;
     private @Mock ShellCommandHandler mShellCommandHandler;
     private @Mock ShellTaskOrganizer mTaskOrganizer;
     private @Mock ShellExecutor mMainExecutor;
@@ -83,7 +85,7 @@
         doReturn(super.mContext.getResources()).when(mContext).getResources();
         mShellInit = spy(new ShellInit(mMainExecutor));
         mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
-                mMainExecutor));
+                mDisplayInsetsController, mMainExecutor));
         mController = new StartingWindowController(mContext, mShellInit, mShellController,
                 mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool);
         mShellInit.init();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index 7c520c3..6292018 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
+import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -35,8 +36,8 @@
 
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
-import com.android.wm.shell.common.ShellExecutor;
 
 import org.junit.After;
 import org.junit.Before;
@@ -63,12 +64,15 @@
     private ShellCommandHandler mShellCommandHandler;
     @Mock
     private Context mTestUserContext;
+    @Mock
+    private DisplayInsetsController mDisplayInsetsController;
 
     private TestShellExecutor mExecutor;
     private ShellController mController;
     private TestConfigurationChangeListener mConfigChangeListener;
     private TestKeyguardChangeListener mKeyguardChangeListener;
     private TestUserChangeListener mUserChangeListener;
+    private TestDisplayImeChangeListener mDisplayImeChangeListener;
 
 
     @Before
@@ -77,8 +81,10 @@
         mKeyguardChangeListener = new TestKeyguardChangeListener();
         mConfigChangeListener = new TestConfigurationChangeListener();
         mUserChangeListener = new TestUserChangeListener();
+        mDisplayImeChangeListener = new TestDisplayImeChangeListener();
         mExecutor = new TestShellExecutor();
-        mController = new ShellController(mContext, mShellInit, mShellCommandHandler, mExecutor);
+        mController = new ShellController(mContext, mShellInit, mShellCommandHandler,
+                mDisplayInsetsController, mExecutor);
         mController.onConfigurationChanged(getConfigurationCopy());
     }
 
@@ -130,6 +136,45 @@
     }
 
     @Test
+    public void testAddDisplayImeChangeListener_ensureCallback() {
+        mController.asShell().addDisplayImeChangeListener(
+                mDisplayImeChangeListener, mExecutor);
+
+        final Rect bounds = new Rect(10, 20, 30, 40);
+        mController.onImeBoundsChanged(bounds);
+        mController.onImeVisibilityChanged(true);
+        mExecutor.flushAll();
+
+        assertTrue(mDisplayImeChangeListener.boundsChanged == 1);
+        assertTrue(bounds.equals(mDisplayImeChangeListener.lastBounds));
+        assertTrue(mDisplayImeChangeListener.visibilityChanged == 1);
+        assertTrue(mDisplayImeChangeListener.lastVisibility);
+    }
+
+    @Test
+    public void testDoubleAddDisplayImeChangeListener_ensureSingleCallback() {
+        mController.asShell().addDisplayImeChangeListener(
+                mDisplayImeChangeListener, mExecutor);
+        mController.asShell().addDisplayImeChangeListener(
+                mDisplayImeChangeListener, mExecutor);
+
+        mController.onImeVisibilityChanged(true);
+        mExecutor.flushAll();
+        assertTrue(mDisplayImeChangeListener.visibilityChanged == 1);
+    }
+
+    @Test
+    public void testAddRemoveDisplayImeChangeListener_ensureNoCallback() {
+        mController.asShell().addDisplayImeChangeListener(
+                mDisplayImeChangeListener, mExecutor);
+        mController.asShell().removeDisplayImeChangeListener(mDisplayImeChangeListener);
+
+        mController.onImeVisibilityChanged(true);
+        mExecutor.flushAll();
+        assertTrue(mDisplayImeChangeListener.visibilityChanged == 0);
+    }
+
+    @Test
     public void testAddUserChangeListener_ensureCallback() {
         mController.addUserChangeListener(mUserChangeListener);
 
@@ -457,4 +502,23 @@
             lastUserProfiles = profiles;
         }
     }
+
+    private static class TestDisplayImeChangeListener implements DisplayImeChangeListener {
+        public int boundsChanged = 0;
+        public Rect lastBounds;
+        public int visibilityChanged = 0;
+        public boolean lastVisibility = false;
+
+        @Override
+        public void onImeBoundsChanged(int displayId, Rect bounds) {
+            boundsChanged++;
+            lastBounds = bounds;
+        }
+
+        @Override
+        public void onImeVisibilityChanged(int displayId, boolean isShowing) {
+            visibilityChanged++;
+            lastVisibility = isShowing;
+        }
+    }
 }
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index 156be38..f33bcb7 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -11,7 +11,7 @@
     name: "location_bypass"
     namespace: "location"
     description: "Enable location bypass appops behavior"
-    bug: "301150056"
+    bug: "329151785"
 }
 
 flag {
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 6cf9c6f..bf39425 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -109,5 +109,5 @@
     name: "enable_null_session_in_media_browser_service"
     namespace: "media_solutions"
     description: "Enables apps owning a MediaBrowserService to disconnect all connected browsers."
-    bug: "263520343"
+    bug: "185136506"
 }
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index d204908..dd2a534 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -393,16 +393,12 @@
     }
 
     /**
-     * Sets a listener to be invoked when an input event is not handled
-     * by the TV AD service.
+     * Sets a listener to be invoked when an input event is not handled by the TV AD service.
      *
      * @param listener The callback to be invoked when the unhandled input event is received.
      */
-    public void setOnUnhandledInputEventListener(
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnUnhandledInputEventListener listener) {
+    public void setOnUnhandledInputEventListener(@NonNull OnUnhandledInputEventListener listener) {
         mOnUnhandledInputEventListener = listener;
-        // TODO: handle CallbackExecutor
     }
 
     /**
@@ -441,6 +437,9 @@
     /**
      * Prepares the AD service of corresponding {@link TvAdService}.
      *
+     * <p>This should be called before calling {@link #startAdService()}. Otherwise,
+     * {@link #startAdService()} is a no-op.
+     *
      * @param serviceId the AD service ID, which can be found in TvAdServiceInfo#getId().
      */
     public void prepareAdService(@NonNull String serviceId, @NonNull String type) {
@@ -455,6 +454,9 @@
 
     /**
      * Starts the AD service.
+     *
+     * <p>This should be called after calling {@link #prepareAdService(String, String)}. Otherwise,
+     * it's a no-op.
      */
     public void startAdService() {
         if (DEBUG) {
@@ -467,6 +469,8 @@
 
     /**
      * Stops the AD service.
+     *
+     * <p>It's a no-op if the service is not started.
      */
     public void stopAdService() {
         if (DEBUG) {
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 6019aa8..42d0cc4 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -52,12 +52,21 @@
     <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
     <string name="title_app_streaming">Allow &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to access this information from your phone</string>
 
+    <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) with mirroring enabled [CHAR LIMIT=NONE] -->
+    <string name="title_app_streaming_with_mirroring">Allow &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to stream your phone\u2019s apps?</string>
+
+    <!-- Summary for associating an application with a companion device of APP_STREAMING profile [CHAR LIMIT=NONE] -->
+    <string name="summary_app_streaming">%1$s will have access to anything that’s visible or played on the phone, including audio, photos, passwords, and messages.&lt;br/>&lt;br/>%1$s will be able to stream apps until you remove access to this permission.</string>
+
     <!-- Title of the helper dialog for APP_STREAMING profile [CHAR LIMIT=30]. -->
     <string name="helper_title_app_streaming">Cross-device services</string>
 
     <!-- Description of the helper dialog for APP_STREAMING profile. [CHAR LIMIT=NONE] -->
     <string name="helper_summary_app_streaming"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="display_name" example="Chromebook">%2$s</xliff:g> to stream apps between your devices</string>
 
+    <!-- Description of the helper dialog for APP_STREAMING profile with mirroring enabled. [CHAR LIMIT=NONE] -->
+    <string name="helper_summary_app_streaming_with_mirroring"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="display_name" example="Chromebook">%2$s</xliff:g> to display and stream apps between your devices</string>
+
     <!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= -->
 
     <!-- Confirmation for associating an application with a companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] -->
@@ -85,6 +94,12 @@
     <!-- Confirmation for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile (type) [CHAR LIMIT=NONE] -->
     <string name="title_nearby_device_streaming">Allow &lt;strong&gt;<xliff:g id="device_name" example="NearbyStreamer">%1$s</xliff:g>&lt;/strong&gt; to take this action?</string>
 
+    <!-- Confirmation for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile (type) with mirroring enabled [CHAR LIMIT=NONE] -->
+    <string name="title_nearby_device_streaming_with_mirroring">Allow &lt;strong&gt;<xliff:g id="device_name" example="NearbyStreamer">%1$s</xliff:g>&lt;/strong&gt; to stream your phone\u2019s apps and system features?</string>
+
+    <!-- Summary for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile [CHAR LIMIT=NONE] -->
+    <string name="summary_nearby_device_streaming">%1$s will have access to anything that’s visible or played on your phone, including audio, photos, payment info, passwords, and messages.&lt;br/>&lt;br/>%1$s will be able to stream apps and system features until you remove access to this permission.</string>
+
     <!-- Description of the helper dialog for NEARBY_DEVICE_STREAMING profile. [CHAR LIMIT=NONE] -->
     <string name="helper_summary_nearby_device_streaming"><xliff:g id="app_name" example="NearbyStreamerApp">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_name" example="NearbyDevice">%2$s</xliff:g> to stream apps and other system features to nearby devices</string>
 
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index 4c1f631..1231b63 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -179,7 +179,7 @@
     // onActivityResult() after the association is created.
     private @Nullable DeviceFilterPair<?> mSelectedDevice;
 
-    private LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this);
+    private final LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this);
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -484,10 +484,18 @@
         }
 
         title = getHtmlFromResources(this, PROFILE_TITLES.get(deviceProfile), deviceName);
+
+        if (PROFILE_SUMMARIES.containsKey(deviceProfile)) {
+            final int summaryResourceId = PROFILE_SUMMARIES.get(deviceProfile);
+            final Spanned summary = getHtmlFromResources(this, summaryResourceId,
+                    deviceName);
+            mSummary.setText(summary);
+        } else {
+            mSummary.setVisibility(View.GONE);
+        }
+
         setupPermissionList(deviceProfile);
 
-        // Summary is not needed for selfManaged dialog.
-        mSummary.setVisibility(View.GONE);
         mTitle.setText(title);
         mVendorHeaderName.setText(vendorName);
         mVendorHeader.setVisibility(View.VISIBLE);
@@ -692,6 +700,11 @@
     private void setupPermissionList(String deviceProfile) {
         final List<Integer> permissionTypes = new ArrayList<>(
                 PROFILE_PERMISSIONS.get(deviceProfile));
+        if (permissionTypes.isEmpty()) {
+            // Nothing to do if there are no permission types.
+            return;
+        }
+
         mPermissionListAdapter.setPermissionType(permissionTypes);
         mPermissionListRecyclerView.setAdapter(mPermissionListAdapter);
         mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager);
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
index 23a11d6..dc68bcc 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
@@ -27,11 +27,13 @@
 import static java.util.Collections.unmodifiableMap;
 import static java.util.Collections.unmodifiableSet;
 
+import android.companion.virtual.flags.Flags;
 import android.os.Build;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -122,10 +124,19 @@
     static final Map<String, Integer> PROFILE_TITLES;
     static {
         final Map<String, Integer> map = new ArrayMap<>();
-        map.put(DEVICE_PROFILE_APP_STREAMING, R.string.title_app_streaming);
+        if (Flags.interactiveScreenMirror()) {
+            map.put(DEVICE_PROFILE_APP_STREAMING, R.string.title_app_streaming_with_mirroring);
+        } else {
+            map.put(DEVICE_PROFILE_APP_STREAMING, R.string.title_app_streaming);
+        }
         map.put(DEVICE_PROFILE_AUTOMOTIVE_PROJECTION, R.string.title_automotive_projection);
         map.put(DEVICE_PROFILE_COMPUTER, R.string.title_computer);
-        map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.title_nearby_device_streaming);
+        if (Flags.interactiveScreenMirror()) {
+            map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING,
+                    R.string.title_nearby_device_streaming_with_mirroring);
+        } else {
+            map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.title_nearby_device_streaming);
+        }
         map.put(DEVICE_PROFILE_WATCH, R.string.confirmation_title);
         map.put(DEVICE_PROFILE_GLASSES, R.string.confirmation_title_glasses);
         map.put(null, R.string.confirmation_title);
@@ -138,6 +149,11 @@
         final Map<String, Integer> map = new ArrayMap<>();
         map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch);
         map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses);
+        if (Flags.interactiveScreenMirror()) {
+            map.put(DEVICE_PROFILE_APP_STREAMING, R.string.summary_app_streaming);
+            map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING,
+                    R.string.summary_nearby_device_streaming);
+        }
         map.put(null, R.string.summary_generic);
 
         PROFILE_SUMMARIES = unmodifiableMap(map);
@@ -146,11 +162,16 @@
     static final Map<String, List<Integer>> PROFILE_PERMISSIONS;
     static {
         final Map<String, List<Integer>> map = new ArrayMap<>();
-        map.put(DEVICE_PROFILE_APP_STREAMING, Arrays.asList(PERMISSION_APP_STREAMING));
         map.put(DEVICE_PROFILE_COMPUTER, Arrays.asList(
                 PERMISSION_NOTIFICATION_LISTENER_ACCESS, PERMISSION_STORAGE));
-        map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING,
-                Arrays.asList(PERMISSION_NEARBY_DEVICE_STREAMING));
+        if (Flags.interactiveScreenMirror()) {
+            map.put(DEVICE_PROFILE_APP_STREAMING, Collections.emptyList());
+            map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, Collections.emptyList());
+        } else {
+            map.put(DEVICE_PROFILE_APP_STREAMING, Arrays.asList(PERMISSION_APP_STREAMING));
+            map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING,
+                    Arrays.asList(PERMISSION_NEARBY_DEVICE_STREAMING));
+        }
         if (Build.VERSION.SDK_INT > UPSIDE_DOWN_CAKE) {
             map.put(DEVICE_PROFILE_WATCH, Arrays.asList(PERMISSION_NOTIFICATIONS, PERMISSION_PHONE,
                     PERMISSION_CALL_LOGS, PERMISSION_SMS, PERMISSION_CONTACTS, PERMISSION_CALENDAR,
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java
index 8f32dbb..fe0e021 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java
@@ -26,6 +26,7 @@
 
 import android.annotation.Nullable;
 import android.companion.AssociationRequest;
+import android.companion.virtual.flags.Flags;
 import android.content.DialogInterface;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
@@ -129,7 +130,9 @@
             case DEVICE_PROFILE_APP_STREAMING:
                 title = getHtmlFromResources(getContext(), R.string.helper_title_app_streaming);
                 summary = getHtmlFromResources(
-                        getContext(), R.string.helper_summary_app_streaming, title, displayName);
+                        getContext(), Flags.interactiveScreenMirror()
+                                ? R.string.helper_summary_app_streaming_with_mirroring
+                                : R.string.helper_summary_app_streaming, title, displayName);
                 break;
 
             case DEVICE_PROFILE_COMPUTER:
diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
index 5d71b7d..37b5d40 100644
--- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
+++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
@@ -38,15 +38,15 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
-import android.util.BackgroundThread;
-import android.util.LongArrayQueue;
 import android.util.Slog;
 import android.util.Xml;
+import android.utils.BackgroundThread;
+import android.utils.LongArrayQueue;
+import android.utils.XmlUtils;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
index 9217e70..0fcec26 100644
--- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
+++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
@@ -29,7 +29,6 @@
 import android.content.pm.VersionedPackage;
 import android.os.Build;
 import android.os.Environment;
-import android.os.FileUtils;
 import android.os.PowerManager;
 import android.os.RecoverySystem;
 import android.os.SystemClock;
@@ -42,10 +41,11 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
+import android.utils.ArrayUtils;
+import android.utils.FileUtils;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.ArrayUtils;
 import com.android.server.PackageWatchdog.FailureReasons;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
 import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
diff --git a/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java
new file mode 100644
index 0000000..fa4d6af
--- /dev/null
+++ b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.io.File;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java
+ *
+ * @hide
+ */
+public class ArrayUtils {
+    private ArrayUtils() { /* cannot be instantiated */ }
+    public static final File[] EMPTY_FILE = new File[0];
+
+
+    /**
+     * Return first index of {@code value} in {@code array}, or {@code -1} if
+     * not found.
+     */
+    public static <T> int indexOf(@Nullable T[] array, T value) {
+        if (array == null) return -1;
+        for (int i = 0; i < array.length; i++) {
+            if (Objects.equals(array[i], value)) return i;
+        }
+        return -1;
+    }
+
+    /** @hide */
+    public static @NonNull File[] defeatNullable(@Nullable File[] val) {
+        return (val != null) ? val : EMPTY_FILE;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable int[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * True if the byte array is null or has length 0.
+     */
+    public static boolean isEmpty(@Nullable byte[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Converts from List of bytes to byte array
+     * @param list
+     * @return byte[]
+     */
+    public static byte[] toPrimitive(List<byte[]> list) {
+        if (list.size() == 0) {
+            return new byte[0];
+        }
+        int byteLen = list.get(0).length;
+        byte[] array = new byte[list.size() * byteLen];
+        for (int i = 0; i < list.size(); i++) {
+            for (int j = 0; j < list.get(i).length; j++) {
+                array[i * byteLen + j] = list.get(i)[j];
+            }
+        }
+        return array;
+    }
+
+    /**
+     * Adds value to given array if not already present, providing set-like
+     * behavior.
+     */
+    public static @NonNull int[] appendInt(@Nullable int[] cur, int val) {
+        return appendInt(cur, val, false);
+    }
+
+    /**
+     * Adds value to given array.
+     */
+    public static @NonNull int[] appendInt(@Nullable int[] cur, int val,
+            boolean allowDuplicates) {
+        if (cur == null) {
+            return new int[] { val };
+        }
+        final int n = cur.length;
+        if (!allowDuplicates) {
+            for (int i = 0; i < n; i++) {
+                if (cur[i] == val) {
+                    return cur;
+                }
+            }
+        }
+        int[] ret = new int[n + 1];
+        System.arraycopy(cur, 0, ret, 0, n);
+        ret[n] = val;
+        return ret;
+    }
+}
diff --git a/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java b/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java
similarity index 99%
rename from packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java
rename to packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java
index a6ae68f..afcf689 100644
--- a/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java
+++ b/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.util;
+package android.utils;
 
 import android.annotation.NonNull;
 import android.os.Handler;
diff --git a/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java
new file mode 100644
index 0000000..e4923bf
--- /dev/null
+++ b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Bits and pieces copied from hidden API of android.os.FileUtils.
+ *
+ * @hide
+ */
+public class FileUtils {
+    /**
+     * Read a text file into a String, optionally limiting the length.
+     *
+     * @param file     to read (will not seek, so things like /proc files are OK)
+     * @param max      length (positive for head, negative of tail, 0 for no limit)
+     * @param ellipsis to add of the file was truncated (can be null)
+     * @return the contents of the file, possibly truncated
+     * @throws IOException if something goes wrong reading the file
+     * @hide
+     */
+    public static @Nullable String readTextFile(@Nullable File file, @Nullable int max,
+            @Nullable String ellipsis) throws IOException {
+        InputStream input = new FileInputStream(file);
+        // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
+        // input stream, bytes read not equal to buffer size is not necessarily the correct
+        // indication for EOF; but it is true for BufferedInputStream due to its implementation.
+        BufferedInputStream bis = new BufferedInputStream(input);
+        try {
+            long size = file.length();
+            if (max > 0 || (size > 0 && max == 0)) {  // "head" mode: read the first N bytes
+                if (size > 0 && (max == 0 || size < max)) max = (int) size;
+                byte[] data = new byte[max + 1];
+                int length = bis.read(data);
+                if (length <= 0) return "";
+                if (length <= max) return new String(data, 0, length);
+                if (ellipsis == null) return new String(data, 0, max);
+                return new String(data, 0, max) + ellipsis;
+            } else if (max < 0) {  // "tail" mode: keep the last N
+                int len;
+                boolean rolled = false;
+                byte[] last = null;
+                byte[] data = null;
+                do {
+                    if (last != null) rolled = true;
+                    byte[] tmp = last;
+                    last = data;
+                    data = tmp;
+                    if (data == null) data = new byte[-max];
+                    len = bis.read(data);
+                } while (len == data.length);
+
+                if (last == null && len <= 0) return "";
+                if (last == null) return new String(data, 0, len);
+                if (len > 0) {
+                    rolled = true;
+                    System.arraycopy(last, len, last, 0, last.length - len);
+                    System.arraycopy(data, 0, last, last.length - len, len);
+                }
+                if (ellipsis == null || !rolled) return new String(last);
+                return ellipsis + new String(last);
+            } else {  // "cat" mode: size unknown, read it all in streaming fashion
+                ByteArrayOutputStream contents = new ByteArrayOutputStream();
+                int len;
+                byte[] data = new byte[1024];
+                do {
+                    len = bis.read(data);
+                    if (len > 0) contents.write(data, 0, len);
+                } while (len == data.length);
+                return contents.toString();
+            }
+        } finally {
+            bis.close();
+            input.close();
+        }
+    }
+
+    /**
+     * Perform an fsync on the given FileOutputStream. The stream at this
+     * point must be flushed but not yet closed.
+     *
+     * @hide
+     */
+    public static boolean sync(FileOutputStream stream) {
+        try {
+            if (stream != null) {
+                stream.getFD().sync();
+            }
+            return true;
+        } catch (IOException e) {
+        }
+        return false;
+    }
+
+    /**
+     * List the files in the directory or return empty file.
+     *
+     * @hide
+     */
+    public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
+        return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles())
+            : ArrayUtils.EMPTY_FILE;
+    }
+}
diff --git a/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java b/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java
similarity index 98%
rename from packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java
rename to packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java
index 948ebcca..fdb15e2 100644
--- a/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java
+++ b/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.util;
+package android.utils;
 
 import android.annotation.NonNull;
 import android.os.Handler;
diff --git a/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java
new file mode 100644
index 0000000..5cdc253
--- /dev/null
+++ b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.utils;
+
+import libcore.util.EmptyArray;
+
+import java.util.NoSuchElementException;
+
+/**
+ * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java
+ *
+ * @hide
+ */
+public class LongArrayQueue {
+
+    private long[] mValues;
+    private int mSize;
+    private int mHead;
+    private int mTail;
+
+    private long[] newUnpaddedLongArray(int num) {
+        return new long[num];
+    }
+    /**
+     * Initializes a queue with the given starting capacity.
+     *
+     * @param initialCapacity the capacity.
+     */
+    public LongArrayQueue(int initialCapacity) {
+        if (initialCapacity == 0) {
+            mValues = EmptyArray.LONG;
+        } else {
+            mValues = newUnpaddedLongArray(initialCapacity);
+        }
+        mSize = 0;
+        mHead = mTail = 0;
+    }
+
+    /**
+     * Initializes a queue with default starting capacity.
+     */
+    public LongArrayQueue() {
+        this(16);
+    }
+
+    /** @hide */
+    public static int growSize(int currentSize) {
+        return currentSize <= 4 ? 8 : currentSize * 2;
+    }
+
+    private void grow() {
+        if (mSize < mValues.length) {
+            throw new IllegalStateException("Queue not full yet!");
+        }
+        final int newSize = growSize(mSize);
+        final long[] newArray = newUnpaddedLongArray(newSize);
+        final int r = mValues.length - mHead; // Number of elements on and to the right of head.
+        System.arraycopy(mValues, mHead, newArray, 0, r);
+        System.arraycopy(mValues, 0, newArray, r, mHead);
+        mValues = newArray;
+        mHead = 0;
+        mTail = mSize;
+    }
+
+    /**
+     * Returns the number of elements in the queue.
+     */
+    public int size() {
+        return mSize;
+    }
+
+    /**
+     * Removes all elements from this queue.
+     */
+    public void clear() {
+        mSize = 0;
+        mHead = mTail = 0;
+    }
+
+    /**
+     * Adds a value to the tail of the queue.
+     *
+     * @param value the value to be added.
+     */
+    public void addLast(long value) {
+        if (mSize == mValues.length) {
+            grow();
+        }
+        mValues[mTail] = value;
+        mTail = (mTail + 1) % mValues.length;
+        mSize++;
+    }
+
+    /**
+     * Removes an element from the head of the queue.
+     *
+     * @return the element at the head of the queue.
+     * @throws NoSuchElementException if the queue is empty.
+     */
+    public long removeFirst() {
+        if (mSize == 0) {
+            throw new NoSuchElementException("Queue is empty!");
+        }
+        final long ret = mValues[mHead];
+        mHead = (mHead + 1) % mValues.length;
+        mSize--;
+        return ret;
+    }
+
+    /**
+     * Returns the element at the given position from the head of the queue, where 0 represents the
+     * head of the queue.
+     *
+     * @param position the position from the head of the queue.
+     * @return the element found at the given position.
+     * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or
+     *                                   {@code position} >= {@link #size()}
+     */
+    public long get(int position) {
+        if (position < 0 || position >= mSize) {
+            throw new IndexOutOfBoundsException("Index " + position
+                + " not valid for a queue of size " + mSize);
+        }
+        final int index = (mHead + position) % mValues.length;
+        return mValues[index];
+    }
+
+    /**
+     * Returns the element at the head of the queue, without removing it.
+     *
+     * @return the element at the head of the queue.
+     * @throws NoSuchElementException if the queue is empty
+     */
+    public long peekFirst() {
+        if (mSize == 0) {
+            throw new NoSuchElementException("Queue is empty!");
+        }
+        return mValues[mHead];
+    }
+
+    /**
+     * Returns the element at the tail of the queue.
+     *
+     * @return the element at the tail of the queue.
+     * @throws NoSuchElementException if the queue is empty.
+     */
+    public long peekLast() {
+        if (mSize == 0) {
+            throw new NoSuchElementException("Queue is empty!");
+        }
+        final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1;
+        return mValues[index];
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        if (mSize <= 0) {
+            return "{}";
+        }
+
+        final StringBuilder buffer = new StringBuilder(mSize * 64);
+        buffer.append('{');
+        buffer.append(get(0));
+        for (int i = 1; i < mSize; i++) {
+            buffer.append(", ");
+            buffer.append(get(i));
+        }
+        buffer.append('}');
+        return buffer.toString();
+    }
+}
diff --git a/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java
new file mode 100644
index 0000000..dbbef61
--- /dev/null
+++ b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.utils;
+
+import android.annotation.NonNull;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.modules.utils.TypedXmlPullParser;
+
+import libcore.util.XmlObjectFactory;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Copied over partly from frameworks/base/core/java/com/android/internal/util/XmlUtils.java
+ *
+ * @hide
+ */
+public class XmlUtils {
+
+    private static final String STRING_ARRAY_SEPARATOR = ":";
+
+    /** @hide */
+    public static final void beginDocument(XmlPullParser parser, String firstElementName)
+            throws XmlPullParserException, IOException {
+        int type;
+        while ((type = parser.next()) != parser.START_TAG
+            && type != parser.END_DOCUMENT) {
+            // Do nothing
+        }
+
+        if (type != parser.START_TAG) {
+            throw new XmlPullParserException("No start tag found");
+        }
+
+        if (!parser.getName().equals(firstElementName)) {
+            throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
+                + ", expected " + firstElementName);
+        }
+    }
+
+    /** @hide */
+    public static boolean nextElementWithin(XmlPullParser parser, int outerDepth)
+            throws IOException, XmlPullParserException {
+        for (;;) {
+            int type = parser.next();
+            if (type == XmlPullParser.END_DOCUMENT
+                    || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) {
+                return false;
+            }
+            if (type == XmlPullParser.START_TAG
+                    && parser.getDepth() == outerDepth + 1) {
+                return true;
+            }
+        }
+    }
+
+    private static XmlPullParser newPullParser() {
+        try {
+            XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+            return parser;
+        } catch (XmlPullParserException e) {
+            throw new AssertionError();
+        }
+    }
+
+    /** @hide */
+    public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
+            throws IOException {
+        final byte[] magic = new byte[4];
+        if (in instanceof FileInputStream) {
+            try {
+                Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
+            } catch (ErrnoException e) {
+                throw e.rethrowAsIOException();
+            }
+        } else {
+            if (!in.markSupported()) {
+                in = new BufferedInputStream(in);
+            }
+            in.mark(8);
+            in.read(magic);
+            in.reset();
+        }
+
+        final TypedXmlPullParser xml;
+        xml = (TypedXmlPullParser) newPullParser();
+        try {
+            xml.setInput(in, "UTF_8");
+        } catch (XmlPullParserException e) {
+            throw new IOException(e);
+        }
+        return xml;
+    }
+}
diff --git a/packages/CredentialManager/wear/res/values/strings.xml b/packages/CredentialManager/wear/res/values/strings.xml
index 9480e64..ee8bb78 100644
--- a/packages/CredentialManager/wear/res/values/strings.xml
+++ b/packages/CredentialManager/wear/res/values/strings.xml
@@ -21,9 +21,6 @@
   <!-- Title of a screen prompting if the user would like to use their saved passkey.
   [CHAR LIMIT=80] -->
   <string name="use_passkey_title">Use passkey?</string>
-  <!-- Title of a screen prompting if the user would like to use their saved passkey.
-[CHAR LIMIT=80] -->
-  <string name="use_sign_in_with_provider_title">Use your sign in for %1$s</string>
   <!-- Title of a screen prompting if the user would like to sign in with provider
   [CHAR LIMIT=80] -->
   <string name="use_password_title">Use password?</string>
@@ -35,6 +32,8 @@
   <string name="dialog_sign_in_options_button">Sign-in Options</string>
   <!-- Title for multiple credentials folded screen. [CHAR LIMIT=NONE] -->
   <string name="sign_in_options_title">Sign-in Options</string>
+  <!-- Provider settings list title. [CHAR LIMIT=NONE] -->
+  <string name="provider_list_title">Manage sign-ins</string>
   <!-- Title for multiple credentials screen. [CHAR LIMIT=NONE] -->
   <string name="choose_sign_in_title">Choose a sign in</string>
   <!-- Title for multiple credentials screen with only passkeys. [CHAR LIMIT=NONE] -->
diff --git a/packages/CredentialManager/wear/robotests/Android.bp b/packages/CredentialManager/wear/robotests/Android.bp
new file mode 100644
index 0000000..c0a1822
--- /dev/null
+++ b/packages/CredentialManager/wear/robotests/Android.bp
@@ -0,0 +1,28 @@
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_robolectric_test {
+    name: "CredentialSelectorTests",
+    srcs: [
+        "src/**/*.kt",
+    ],
+    // Include test libraries.
+    instrumentation_for: "ClockworkCredentialManager",
+    libs: [
+        "androidx.test.runner",
+        "androidx.test.ext.junit",
+        "kotlinx_coroutines_android",
+        "kotlinx_coroutines",
+        "kotlinx-coroutines-core",
+        "kotlinx_coroutines_test",
+        "mockito-robolectric-prebuilt",
+        "mockito-kotlin2",
+        "CredentialManagerShared",
+        "ClockworkCredentialManager",
+        "framework_graphics_flags_java_lib",
+    ],
+    java_resource_dirs: ["config"],
+    upstream: true,
+}
diff --git a/packages/CredentialManager/wear/robotests/config/robolectric.properties b/packages/CredentialManager/wear/robotests/config/robolectric.properties
new file mode 100644
index 0000000..140e42b
--- /dev/null
+++ b/packages/CredentialManager/wear/robotests/config/robolectric.properties
@@ -0,0 +1,16 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+sdk=NEWEST_SDK
+
diff --git a/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt
new file mode 100644
index 0000000..3422d3d
--- /dev/null
+++ b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.credentialmanager
+
+import java.time.Instant
+import android.graphics.drawable.Drawable
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.model.get.ActionEntryInfo
+import com.android.credentialmanager.model.get.AuthenticationEntryInfo
+import com.android.credentialmanager.model.Request
+import androidx.test.filters.SmallTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.junit.runner.RunWith
+import com.android.credentialmanager.model.CredentialType
+import com.google.common.truth.Truth.assertThat
+import com.android.credentialmanager.ui.mappers.toGet
+import com.android.credentialmanager.model.get.ProviderInfo
+import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerUserNameEntries
+
+/** Unit tests for [CredentialSelectorUiStateGetMapper]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CredentialSelectorUiStateGetMapperTest {
+
+    private val mDrawable = mock<Drawable>()
+
+    private val actionEntryInfo =
+        ActionEntryInfo(
+            providerId = "",
+            entryKey = "",
+            entrySubkey = "",
+            pendingIntent = null,
+            fillInIntent = null,
+            title = "title",
+            icon = mDrawable,
+            subTitle = "subtitle",
+        )
+
+    private val authenticationEntryInfo =
+        AuthenticationEntryInfo(
+            providerId = "",
+            entryKey = "",
+            entrySubkey = "",
+            pendingIntent = null,
+            fillInIntent = null,
+            title = "title",
+            providerDisplayName = "",
+            icon = mDrawable,
+            isUnlockedAndEmpty = true,
+            isLastUnlocked = true
+        )
+
+    val passkeyCredentialEntryInfo =
+        createCredentialEntryInfo(credentialType = CredentialType.PASSKEY, userName = "userName")
+
+    val unknownCredentialEntryInfo =
+        createCredentialEntryInfo(credentialType = CredentialType.UNKNOWN, userName = "userName2")
+
+    val passwordCredentialEntryInfo =
+        createCredentialEntryInfo(credentialType = CredentialType.PASSWORD, userName = "userName")
+
+    val recentlyUsedPasskeyCredential =
+        createCredentialEntryInfo(credentialType =
+    CredentialType.PASSKEY, lastUsedTimeMillis = 2L, userName = "userName")
+
+    val recentlyUsedPasswordCredential =
+        createCredentialEntryInfo(credentialType =
+    CredentialType.PASSWORD, lastUsedTimeMillis = 2L, userName = "userName")
+
+    val credentialList1 = listOf(
+        passkeyCredentialEntryInfo,
+        passwordCredentialEntryInfo
+    )
+
+    val credentialList2 = listOf(
+        passkeyCredentialEntryInfo,
+        passwordCredentialEntryInfo,
+        recentlyUsedPasskeyCredential,
+        unknownCredentialEntryInfo,
+        recentlyUsedPasswordCredential
+    )
+
+    @Test
+    fun `On primary screen, just one account returns SingleEntry`() {
+        val getCredentialUiState = Request.Get(
+            token = null,
+            resultReceiver = null,
+            finalResponseReceiver = null,
+            providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = true)
+
+        assertThat(getCredentialUiState).isEqualTo(
+            CredentialSelectorUiState.Get.SingleEntry(passkeyCredentialEntryInfo)
+        ) // prefer passkey over password for selected credential
+    }
+
+    @Test
+    fun `On primary screen, multiple accounts returns SingleEntryPerAccount`() {
+        val getCredentialUiState = Request.Get(
+            token = null,
+            resultReceiver = null,
+            finalResponseReceiver = null,
+            providerInfos = listOf(createProviderInfo(listOf(passkeyCredentialEntryInfo,
+                unknownCredentialEntryInfo)))).toGet(isPrimary = true)
+
+        assertThat(getCredentialUiState).isEqualTo(
+            CredentialSelectorUiState.Get.SingleEntryPerAccount(
+                sortedEntries = listOf(
+                    passkeyCredentialEntryInfo, // userName
+                    unknownCredentialEntryInfo // userName2
+                ),
+                authenticationEntryList = listOf(authenticationEntryInfo)
+            )) // prefer passkey from account 1, then unknown from account 2
+    }
+
+    @Test
+    fun `On secondary screen, a MultipleEntry is returned`() {
+        val getCredentialUiState = Request.Get(
+            token = null,
+            resultReceiver = null,
+            finalResponseReceiver = null,
+            providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = false)
+
+        assertThat(getCredentialUiState).isEqualTo(
+            CredentialSelectorUiState.Get.MultipleEntry(
+                listOf(PerUserNameEntries("userName", listOf(
+                    passkeyCredentialEntryInfo,
+                    passwordCredentialEntryInfo))
+                ),
+                listOf(actionEntryInfo),
+                listOf(authenticationEntryInfo)
+            ))
+    }
+
+    @Test
+    fun `Returned multiple entry is sorted by credentialType and lastUsedTimeMillis`() {
+        val getCredentialUiState = Request.Get(
+            token = null,
+            resultReceiver = null,
+            finalResponseReceiver = null,
+            providerInfos = listOf(createProviderInfo(credentialList1),
+                createProviderInfo(credentialList2))).toGet(isPrimary = false)
+
+        assertThat(getCredentialUiState).isEqualTo(
+            CredentialSelectorUiState.Get.MultipleEntry(
+                listOf(
+                    PerUserNameEntries("userName",
+                        listOf(
+                            recentlyUsedPasskeyCredential, // from provider 2
+                            passkeyCredentialEntryInfo, // from provider 1 or 2
+                            passkeyCredentialEntryInfo, // from provider 1 or 2
+                            recentlyUsedPasswordCredential, // from provider 2
+                            passwordCredentialEntryInfo, // from provider 1 or 2
+                            passwordCredentialEntryInfo, // from provider 1 or 2
+                        )),
+                    PerUserNameEntries("userName2", listOf(unknownCredentialEntryInfo)),
+                ),
+                listOf(actionEntryInfo, actionEntryInfo),
+                listOf(authenticationEntryInfo, authenticationEntryInfo)
+            )
+        )
+    }
+
+    fun createCredentialEntryInfo(
+        userName: String,
+        credentialType: CredentialType = CredentialType.PASSKEY,
+        lastUsedTimeMillis: Long = 0L
+    ): CredentialEntryInfo =
+        CredentialEntryInfo(
+            providerId = "",
+            entryKey = "",
+            entrySubkey = "",
+            pendingIntent = null,
+            fillInIntent = null,
+            credentialType = credentialType,
+            rawCredentialType = "",
+            credentialTypeDisplayName = "",
+            providerDisplayName = "",
+            userName = userName,
+            displayName = "",
+            icon = mDrawable,
+            shouldTintIcon = false,
+            lastUsedTimeMillis = Instant.ofEpochMilli(lastUsedTimeMillis),
+            isAutoSelectable = true,
+            entryGroupId = "",
+            isDefaultIconPreferredAsSingleProvider = false,
+            affiliatedDomain = "",
+        )
+
+    fun createProviderInfo(credentials: List<CredentialEntryInfo> = listOf()): ProviderInfo =
+        ProviderInfo(
+            id = "providerInfo",
+            icon = mDrawable,
+            displayName = "displayName",
+            credentialEntryList = credentials,
+            authenticationEntryList = listOf(authenticationEntryInfo),
+            remoteEntry = null,
+            actionEntryList = listOf(actionEntryInfo)
+        )
+}
diff --git a/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorViewModelTest.kt b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorViewModelTest.kt
new file mode 100644
index 0000000..b79f34c
--- /dev/null
+++ b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorViewModelTest.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.credentialmanager
+
+import org.mockito.kotlin.whenever
+import com.android.credentialmanager.model.EntryInfo
+import com.android.credentialmanager.model.Request
+import androidx.test.filters.SmallTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.Before
+import java.util.Collections.emptyList
+import org.junit.runner.RunWith
+import android.content.Intent
+import com.android.credentialmanager.client.CredentialManagerClient
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import android.credentials.selection.BaseDialogResult
+import com.google.common.truth.Truth.assertThat
+import org.mockito.kotlin.doReturn
+import kotlinx.coroutines.Job
+import org.junit.After
+import org.robolectric.shadows.ShadowLooper
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Unit tests for [CredentialSelectorViewModel]. */
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CredentialSelectorViewModelTest {
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    private val stateFlow: MutableStateFlow<Request?> = MutableStateFlow(Request.Create(null))
+    private val credentialManagerClient = mock<CredentialManagerClient>{
+        on { requests } doReturn stateFlow
+    }
+    private val mViewModel = CredentialSelectorViewModel(credentialManagerClient)
+    private lateinit var job: Job
+
+    val testEntryInfo =
+        EntryInfo(
+            providerId = "",
+            entryKey = "",
+            entrySubkey = "",
+            pendingIntent = null,
+            fillInIntent = null,
+            shouldTerminateUiUponSuccessfulProviderResult = true)
+
+    @Before
+    fun setUp() {
+      job = checkNotNull(mViewModel).uiState.launchIn(testScope)
+    }
+
+    @After
+    fun teardown() {
+        job.cancel()
+    }
+
+    @Test
+    fun `Setting state to idle when receiving null request`() {
+        stateFlow.value = null
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Idle)
+    }
+
+    @Test
+    fun `Setting state to cancel when receiving Cancel request`() {
+        stateFlow.value = Request.Cancel(appName = "appName", token = null)
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value)
+            .isEqualTo(CredentialSelectorUiState.Cancel("appName"))
+    }
+
+    @Test
+    fun `Setting state to create when receiving Create request`() {
+        stateFlow.value = Request.Create(token = null)
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Create)
+    }
+
+    @Test
+    fun `Closing app when receiving Close request`() {
+        stateFlow.value = Request.Close(token = null)
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Updates request`() {
+        val intent = Intent()
+
+        mViewModel.updateRequest(intent)
+
+        verify(credentialManagerClient).updateRequest(intent)
+    }
+
+    @Test
+    fun `Back on a single entry screen closes app`() {
+        mViewModel.openSecondaryScreen()
+        stateFlow.value = Request.Get(
+            token = null,
+            resultReceiver = null,
+            finalResponseReceiver = null,
+            providerInfos = emptyList())
+
+        mViewModel.back()
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Back on a multiple entry screen gets us back to a primary screen`() {
+        mViewModel.openSecondaryScreen()
+        stateFlow.value = Request.Get(
+            token = null,
+            resultReceiver = null,
+            finalResponseReceiver = null,
+            providerInfos = emptyList())
+
+        mViewModel.back()
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Back on create request state closes app`() {
+        stateFlow.value = Request.Create(token = null)
+
+        mViewModel.back()
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Back on close request state closes app`() {
+        stateFlow.value = Request.Close(token = null)
+
+        mViewModel.back()
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Back on cancel request state closes app`() {
+        stateFlow.value = Request.Cancel(appName = "", token = null)
+
+        mViewModel.back()
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Back on idle request state closes app`() {
+        stateFlow.value = null
+
+        mViewModel.back()
+        ShadowLooper.idleMainLooper()
+
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Cancel closes the app`() {
+        mViewModel.cancel()
+        ShadowLooper.idleMainLooper()
+
+        verify(credentialManagerClient).sendError(BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED)
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Send entry selection result closes app and calls client method`() {
+        whenever(credentialManagerClient.sendEntrySelectionResult(
+            entryInfo = testEntryInfo,
+            resultCode = null,
+            resultData = null,
+            isAutoSelected = false
+        )).thenReturn(true)
+
+        mViewModel.sendSelectionResult(
+            entryInfo = testEntryInfo,
+            resultCode = null,
+            resultData = null,
+            isAutoSelected = false)
+        ShadowLooper.idleMainLooper()
+
+        verify(credentialManagerClient).sendEntrySelectionResult(
+            testEntryInfo, null, null, false
+        )
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Close)
+    }
+
+    @Test
+    fun `Send entry selection result does not close app on false return`() {
+        whenever(credentialManagerClient.sendEntrySelectionResult(
+            entryInfo = testEntryInfo,
+            resultCode = null,
+            resultData = null,
+            isAutoSelected = false
+        )).thenReturn(false)
+        stateFlow.value = Request.Create(null)
+
+        mViewModel.sendSelectionResult(entryInfo = testEntryInfo, resultCode = null,
+            resultData = null, isAutoSelected = false)
+        ShadowLooper.idleMainLooper()
+
+        verify(credentialManagerClient).sendEntrySelectionResult(
+            entryInfo = testEntryInfo,
+            resultCode = null,
+            resultData = null,
+            isAutoSelected = false
+        )
+        assertThat(mViewModel.uiState.value).isEqualTo(CredentialSelectorUiState.Create)
+    }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 66be7ba..9d97763 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -59,8 +59,10 @@
             isPrimaryScreen,
             shouldClose
         ) { request, isPrimary, shouldClose ->
+            Log.d(TAG, "Request updated: " + request?.toString() +
+                    " isClose: " + shouldClose.toString() +
+                    " isPrimaryScreen: " + isPrimary.toString())
             if (shouldClose) {
-                Log.d(TAG, "Request finished, closing ")
                 return@combine Close
             }
 
@@ -139,7 +141,10 @@
     data object Idle : CredentialSelectorUiState()
     sealed class Get : CredentialSelectorUiState() {
         data class SingleEntry(val entry: CredentialEntryInfo) : Get()
-        data class SingleEntryPerAccount(val sortedEntries: List<CredentialEntryInfo>) : Get()
+        data class SingleEntryPerAccount(
+            val sortedEntries: List<CredentialEntryInfo>,
+            val authenticationEntryList: List<AuthenticationEntryInfo>,
+            ) : Get()
         data class MultipleEntry(
             val accounts: List<PerUserNameEntries>,
             val actionEntryList: List<ActionEntryInfo>,
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
index 405de1d3..bf4c988 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -29,6 +29,7 @@
 import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
 import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState
 import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntryPerAccount
 import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry
 import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry
 import com.android.credentialmanager.CredentialSelectorViewModel
@@ -45,6 +46,8 @@
 import com.android.credentialmanager.model.CredentialType
 import com.android.credentialmanager.model.EntryInfo
 import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFoldScreen
+import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFlattenScreen
+
 
 @OptIn(ExperimentalHorologistApi::class)
 @Composable
@@ -78,59 +81,70 @@
 
         scrollable(Screen.SinglePasskeyScreen.route) {
             SinglePasskeyScreen(
-                credentialSelectorUiState = viewModel.uiState.value as SingleEntry,
+                entry = (remember { uiState } as SingleEntry).entry,
                 columnState = it.columnState,
+                flowEngine = flowEngine,
             )
         }
 
         scrollable(Screen.SignInWithProviderScreen.route) {
             SignInWithProviderScreen(
-                credentialSelectorUiState = viewModel.uiState.value as SingleEntry,
+                entry = (remember { uiState } as SingleEntry).entry,
                 columnState = it.columnState,
+                flowEngine = flowEngine,
             )
         }
 
         scrollable(Screen.MultipleCredentialsScreenFold.route) {
             MultiCredentialsFoldScreen(
-                credentialSelectorUiState = viewModel.uiState.value as MultipleEntry,
-                screenIcon = null,
+                credentialSelectorUiState = (remember { uiState } as SingleEntryPerAccount),
                 columnState = it.columnState,
+                flowEngine = flowEngine,
+            )
+        }
+
+        scrollable(Screen.MultipleCredentialsScreenFlatten.route) {
+            MultiCredentialsFlattenScreen(
+                credentialSelectorUiState = (remember { uiState } as MultipleEntry),
+                columnState = it.columnState,
+                flowEngine = flowEngine,
             )
         }
     }
-    BackHandler(true) {
-        viewModel.back()
-    }
-    Log.d(TAG, "uiState change, state: $uiState")
-    when (val state = uiState) {
-        CredentialSelectorUiState.Idle -> {
-            if (navController.currentDestination?.route != Screen.Loading.route) {
-                navController.navigateToLoading()
+        BackHandler(true) {
+            viewModel.back()
+        }
+        Log.d(TAG, "uiState change, state: $uiState")
+        when (val state = uiState) {
+            CredentialSelectorUiState.Idle -> {
+                if (navController.currentDestination?.route != Screen.Loading.route) {
+                    navController.navigateToLoading()
+                }
+            }
+
+            is CredentialSelectorUiState.Get -> {
+                handleGetNavigation(
+                    navController = navController,
+                    state = state,
+                    onCloseApp = onCloseApp,
+                    selectEntry = selectEntry
+                )
+            }
+
+            CredentialSelectorUiState.Create -> {
+                // TODO: b/301206624 - Implement create flow
+                onCloseApp()
+            }
+
+            is CredentialSelectorUiState.Cancel -> {
+                onCloseApp()
+            }
+
+            CredentialSelectorUiState.Close -> {
+                onCloseApp()
             }
         }
-        is CredentialSelectorUiState.Get -> {
-            handleGetNavigation(
-                navController = navController,
-                state = state,
-                onCloseApp = onCloseApp,
-                selectEntry = selectEntry
-            )
-        }
-
-        CredentialSelectorUiState.Create -> {
-            // TODO: b/301206624 - Implement create flow
-            onCloseApp()
-        }
-
-        is CredentialSelectorUiState.Cancel -> {
-            onCloseApp()
-        }
-
-        CredentialSelectorUiState.Close -> {
-            onCloseApp()
-        }
     }
-}
 
 private fun handleGetNavigation(
     navController: NavController,
@@ -157,13 +171,12 @@
             }
         }
 
-        is MultipleEntry -> {
-            navController.navigateToMultipleCredentialsFoldScreen()
-        }
+            is SingleEntryPerAccount -> {
+                navController.navigateToMultipleCredentialsFoldScreen()
+            }
 
-        else -> {
-            // TODO: b/301206470 - Implement other get flows
-            onCloseApp()
+            is MultipleEntry -> {
+                navController.navigateToMultipleCredentialsFlattenScreen()
+            }
         }
     }
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt
index 03b0931..7a936b6 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt
@@ -32,11 +32,14 @@
     return if (isPrimary) {
         if (accounts.size == 1) {
             CredentialSelectorUiState.Get.SingleEntry(
-                accounts[0].value.minWith(comparator)
+                entry = accounts[0].value.minWith(comparator)
             )
         } else {
             CredentialSelectorUiState.Get.SingleEntryPerAccount(
-                accounts.map { it.value.minWith(comparator) }.sortedWith(comparator)
+                sortedEntries = accounts.map {
+                    it.value.minWith(comparator)
+                }.sortedWith(comparator),
+                authenticationEntryList = providerInfos.flatMap { it.authenticationEntryList }
             )
         }
     } else {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
index 11188b4..d54103c 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
@@ -15,112 +15,53 @@
  */
 package com.android.credentialmanager.ui.screens.multiple
 
-import android.graphics.drawable.Drawable
-import com.android.credentialmanager.ui.screens.UiState
-import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.Text
 import com.android.credentialmanager.ui.components.SignInHeader
 import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry
+import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.R
-import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
-import com.android.credentialmanager.model.get.ActionEntryInfo
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.ui.components.CredentialsScreenChip
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import com.google.android.horologist.compose.layout.ScalingLazyColumn
 import com.google.android.horologist.compose.layout.ScalingLazyColumnState
 
-
 /**
  * Screen that shows multiple credentials to select from, grouped by accounts
  *
  * @param credentialSelectorUiState The app bar view model.
- * @param screenIcon The view model corresponding to the home page.
  * @param columnState ScalingLazyColumn configuration to be be applied
  * @param modifier styling for composable
- * @param viewModel ViewModel that updates ui state for this screen
- * @param navController handles navigation events from this screen
+ * @param flowEngine [FlowEngine] that updates ui state for this screen
  */
 @OptIn(ExperimentalHorologistApi::class)
 @Composable
 fun MultiCredentialsFlattenScreen(
     credentialSelectorUiState: MultipleEntry,
-    screenIcon: Drawable?,
     columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
-    viewModel: MultiCredentialsFlattenViewModel = hiltViewModel(),
-    navController: NavHostController = rememberNavController(),
+    flowEngine: FlowEngine,
 ) {
-    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
-    when (val state = uiState) {
-        UiState.CredentialScreen -> {
-            MultiCredentialsFlattenScreen(
-                state = credentialSelectorUiState,
-                columnState = columnState,
-                screenIcon = screenIcon,
-                onActionEntryClicked = viewModel::onActionEntryClicked,
-                onCredentialClicked = viewModel::onCredentialClicked,
-                modifier = modifier,
-            )
-        }
-
-        is UiState.CredentialSelected -> {
-            val launcher = rememberLauncherForActivityResult(
-                StartBalIntentSenderForResultContract()
-            ) {
-                viewModel.onInfoRetrieved(it.resultCode, null)
-            }
-
-            SideEffect {
-                state.intentSenderRequest?.let {
-                    launcher.launch(it)
-                }
-            }
-        }
-
-        UiState.Cancel -> {
-            navController.popBackStack()
-        }
-    }
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@Composable
-fun MultiCredentialsFlattenScreen(
-    state: MultipleEntry,
-    columnState: ScalingLazyColumnState,
-    screenIcon: Drawable?,
-    onActionEntryClicked: (entryInfo: ActionEntryInfo) -> Unit,
-    onCredentialClicked: (entryInfo: CredentialEntryInfo) -> Unit,
-    modifier: Modifier,
-) {
+    val selectEntry = flowEngine.getEntrySelector()
     ScalingLazyColumn(
         columnState = columnState,
-        modifier = modifier.fillMaxSize(),
+        modifier = Modifier.fillMaxSize(),
     ) {
         item {
             // make this credential specific if all credentials are same
             SignInHeader(
-                icon = screenIcon,
+                icon = null,
                 title = stringResource(R.string.sign_in_options_title),
             )
         }
 
-        state.accounts.forEach { userNameEntries ->
+        credentialSelectorUiState.accounts.forEach { userNameEntries ->
             item {
                 Text(
                     text = userNameEntries.userName,
@@ -135,17 +76,16 @@
                 item {
                     CredentialsScreenChip(
                         label = credential.userName,
-                        onClick = { onCredentialClicked(credential) },
-                        secondaryLabel = credential.userName,
+                        onClick = { selectEntry(credential, false) },
+                        secondaryLabel = credential.credentialTypeDisplayName,
                         icon = credential.icon,
-                        modifier = modifier,
                     )
                 }
             }
         }
         item {
             Text(
-                text = "Manage Sign-ins",
+                text = stringResource(R.string.provider_list_title),
                 modifier = Modifier
                     .padding(top = 6.dp)
                     .padding(horizontal = 10.dp),
@@ -153,14 +93,13 @@
             )
         }
 
-        state.actionEntryList.forEach {
+        credentialSelectorUiState.actionEntryList.forEach {actionEntry ->
             item {
                     CredentialsScreenChip(
-                        label = it.title,
-                        onClick = { onActionEntryClicked(it) },
+                        label = actionEntry.title,
+                        onClick = { selectEntry(actionEntry, false) },
                         secondaryLabel = null,
-                        icon = it.icon,
-                        modifier = modifier,
+                        icon = actionEntry.icon,
                     )
             }
         }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenViewModel.kt
deleted file mode 100644
index ee5f3f4..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenViewModel.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.screens.multiple
-
-import android.content.Intent
-import android.credentials.selection.ProviderPendingIntentResponse
-import android.credentials.selection.UserSelectionDialogResult
-import androidx.lifecycle.ViewModel
-import com.android.credentialmanager.client.CredentialManagerClient
-import com.android.credentialmanager.ktx.getIntentSenderRequest
-import com.android.credentialmanager.model.Request
-import com.android.credentialmanager.model.get.ActionEntryInfo
-import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.ui.screens.UiState
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import javax.inject.Inject
-
-/** ViewModel for [MultiCredentialsFlattenScreen].*/
-@HiltViewModel
-class MultiCredentialsFlattenViewModel @Inject constructor(
-    private val credentialManagerClient: CredentialManagerClient,
-) : ViewModel() {
-
-    private lateinit var requestGet: Request.Get
-    private lateinit var entryInfo: CredentialEntryInfo
-
-    private val _uiState =
-        MutableStateFlow<UiState>(UiState.CredentialScreen)
-    val uiState: StateFlow<UiState> = _uiState
-
-    fun onCredentialClicked(entryInfo: CredentialEntryInfo) {
-        this.entryInfo = entryInfo
-        _uiState.value = UiState.CredentialSelected(
-            intentSenderRequest = entryInfo.getIntentSenderRequest()
-        )
-    }
-
-    fun onCancelClicked() {
-        _uiState.value = UiState.Cancel
-    }
-
-    fun onInfoRetrieved(
-        resultCode: Int? = null,
-        resultData: Intent? = null,
-    ) {
-        val userSelectionDialogResult = UserSelectionDialogResult(
-            requestGet.token,
-            entryInfo.providerId,
-            entryInfo.entryKey,
-            entryInfo.entrySubkey,
-            if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
-        )
-        credentialManagerClient.sendResult(userSelectionDialogResult)
-    }
-
-    fun onActionEntryClicked(actionEntryInfo: ActionEntryInfo) {
-        // TODO(b/322797032)to be filled out
-    }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
index 5515c86..6f32c99 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
@@ -16,24 +16,15 @@
 
 package com.android.credentialmanager.ui.screens.multiple
 
-import com.android.credentialmanager.ui.screens.UiState
-import android.graphics.drawable.Drawable
-import androidx.activity.compose.rememberLauncherForActivityResult
 import com.android.credentialmanager.R
 import androidx.compose.ui.res.stringResource
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
 import com.android.credentialmanager.CredentialSelectorUiState
-import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.ui.components.DismissChip
 import com.android.credentialmanager.ui.components.CredentialsScreenChip
@@ -49,74 +40,22 @@
  * Screen that shows multiple credentials to select from.
  *
  * @param credentialSelectorUiState The app bar view model.
- * @param screenIcon The view model corresponding to the home page.
  * @param columnState ScalingLazyColumn configuration to be be applied
- * @param modifier styling for composable
- * @param viewModel ViewModel that updates ui state for this screen
- * @param navController handles navigation events from this screen
  */
 @OptIn(ExperimentalHorologistApi::class)
 @Composable
 fun MultiCredentialsFoldScreen(
-    credentialSelectorUiState: CredentialSelectorUiState.Get.MultipleEntry,
-    screenIcon: Drawable?,
+    credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntryPerAccount,
     columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
-    viewModel: MultiCredentialsFoldViewModel = hiltViewModel(),
-    navController: NavHostController = rememberNavController(),
+    flowEngine: FlowEngine,
 ) {
-    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
-    when (val state = uiState) {
-        UiState.CredentialScreen -> {
-            MultiCredentialsFoldScreen(
-                state = credentialSelectorUiState,
-                onSignInOptionsClicked = viewModel::onSignInOptionsClicked,
-                onCredentialClicked = viewModel::onCredentialClicked,
-                onCancelClicked = viewModel::onCancelClicked,
-                screenIcon = screenIcon,
-                columnState = columnState,
-                modifier = modifier
-            )
-        }
-
-        is UiState.CredentialSelected -> {
-            val launcher = rememberLauncherForActivityResult(
-                StartBalIntentSenderForResultContract()
-            ) {
-                viewModel.onInfoRetrieved(it.resultCode, null)
-            }
-
-            SideEffect {
-                state.intentSenderRequest?.let {
-                    launcher.launch(it)
-                }
-            }
-        }
-
-        UiState.Cancel -> {
-            navController.popBackStack()
-        }
-    }
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@Composable
-fun MultiCredentialsFoldScreen(
-    state: CredentialSelectorUiState.Get.MultipleEntry,
-    onSignInOptionsClicked: () -> Unit,
-    onCredentialClicked: (entryInfo: CredentialEntryInfo) -> Unit,
-    onCancelClicked: () -> Unit,
-    screenIcon: Drawable?,
-    columnState: ScalingLazyColumnState,
-    modifier: Modifier,
-) {
+    val selectEntry = flowEngine.getEntrySelector()
     ScalingLazyColumn(
         columnState = columnState,
-        modifier = modifier.fillMaxSize(),
+        modifier = Modifier.fillMaxSize(),
     ) {
         // flatten all credentials into one
-        val credentials = state.accounts.flatMap { it.sortedCredentialEntryList }
+        val credentials = credentialSelectorUiState.sortedEntries
         item {
             var title = stringResource(R.string.choose_sign_in_title)
             if (credentials.all{ it.credentialType == CredentialType.PASSKEY }) {
@@ -126,7 +65,7 @@
             }
 
             SignInHeader(
-                icon = screenIcon,
+                icon = null,
                 title = title,
                 modifier = Modifier
                     .padding(top = 6.dp),
@@ -137,22 +76,24 @@
                 item {
                     CredentialsScreenChip(
                         label = credential.userName,
-                        onClick = { onCredentialClicked(credential) },
+                        onClick = { selectEntry(credential, false) },
                         secondaryLabel = credential.credentialTypeDisplayName,
                         icon = credential.icon,
                     )
                 }
             }
 
-        state.authenticationEntryList.forEach { authenticationEntryInfo ->
+        credentialSelectorUiState.authenticationEntryList.forEach { authenticationEntryInfo ->
             item {
                 LockedProviderChip(authenticationEntryInfo) {
-                    // TODO(b/322797032) invoke LockedProviderScreen here using flow engine
-                }
+                    selectEntry(authenticationEntryInfo, false) }
             }
         }
-
-        item { SignInOptionsChip(onSignInOptionsClicked)}
-        item { DismissChip(onCancelClicked) }
+        item {
+            SignInOptionsChip { flowEngine.openSecondaryScreen() }
+        }
+        item {
+            DismissChip { flowEngine.cancel() }
+        }
     }
 }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt
deleted file mode 100644
index 627a63d..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.screens.multiple
-
-import android.content.Intent
-import android.credentials.selection.ProviderPendingIntentResponse
-import android.credentials.selection.UserSelectionDialogResult
-import androidx.lifecycle.ViewModel
-import com.android.credentialmanager.client.CredentialManagerClient
-import com.android.credentialmanager.ktx.getIntentSenderRequest
-import com.android.credentialmanager.model.Request
-import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.ui.screens.UiState
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import javax.inject.Inject
-
-/** ViewModel for [MultiCredentialsFoldScreen].*/
-@HiltViewModel
-class MultiCredentialsFoldViewModel @Inject constructor(
-    private val credentialManagerClient: CredentialManagerClient,
-) : ViewModel() {
-
-    private lateinit var requestGet: Request.Get
-    private lateinit var entryInfo: CredentialEntryInfo
-
-    private val _uiState =
-        MutableStateFlow<UiState>(UiState.CredentialScreen)
-    val uiState: StateFlow<UiState> = _uiState
-
-    fun onCredentialClicked(entryInfo: CredentialEntryInfo) {
-        this.entryInfo = entryInfo
-        _uiState.value = UiState.CredentialSelected(
-            intentSenderRequest = entryInfo.getIntentSenderRequest()
-        )
-    }
-
-    fun onSignInOptionsClicked() {
-        // TODO(b/322797032) Implement navigation route for single credential screen to multiple
-        // credentials
-    }
-
-    fun onCancelClicked() {
-        _uiState.value = UiState.Cancel
-    }
-
-    fun onInfoRetrieved(
-        resultCode: Int? = null,
-        resultData: Intent? = null,
-    ) {
-        val userSelectionDialogResult = UserSelectionDialogResult(
-            requestGet.token,
-            entryInfo.providerId,
-            entryInfo.entryKey,
-            entryInfo.entrySubkey,
-            if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
-        )
-        credentialManagerClient.sendResult(userSelectionDialogResult)
-    }
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
index b2595a1..e79176b 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
@@ -19,122 +19,62 @@
 package com.android.credentialmanager.ui.screens.single.passkey
 
 import androidx.compose.foundation.layout.Column
-import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
-import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.R
-import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
 import com.android.credentialmanager.ui.components.AccountRow
 import com.android.credentialmanager.ui.components.ContinueChip
 import com.android.credentialmanager.ui.components.DismissChip
 import com.android.credentialmanager.ui.components.SignInHeader
 import com.android.credentialmanager.ui.components.SignInOptionsChip
 import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
-import com.android.credentialmanager.ui.screens.UiState
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import com.google.android.horologist.compose.layout.ScalingLazyColumnState
 
 /**
  * Screen that shows sign in with provider credential.
  *
- * @param credentialSelectorUiState The app bar view model.
+ * @param entry The password entry
  * @param columnState ScalingLazyColumn configuration to be be applied to SingleAccountScreen
  * @param modifier styling for composable
- * @param viewModel ViewModel that updates ui state for this screen
- * @param navController handles navigation events from this screen
+ * @param flowEngine [FlowEngine] that updates ui state for this screen
  */
 @OptIn(ExperimentalHorologistApi::class)
 @Composable
 fun SinglePasskeyScreen(
-    credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry,
-    columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
-    viewModel: SinglePasskeyScreenViewModel = hiltViewModel(),
-    navController: NavHostController = rememberNavController(),
-) {
-    viewModel.initialize(credentialSelectorUiState.entry)
-
-    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
-    when (val state = uiState) {
-        UiState.CredentialScreen -> {
-            SinglePasskeyScreen(
-                credentialSelectorUiState.entry,
-                columnState,
-                modifier,
-                viewModel
-            )
-        }
-
-        is UiState.CredentialSelected -> {
-            val launcher = rememberLauncherForActivityResult(
-                StartBalIntentSenderForResultContract()
-            ) {
-                viewModel.onPasskeyInfoRetrieved(it.resultCode, null)
-            }
-
-            SideEffect {
-                state.intentSenderRequest?.let {
-                    launcher.launch(it)
-                }
-            }
-        }
-
-        UiState.Cancel -> {
-            // TODO(b/322797032) add valid navigation path here for going back
-            navController.popBackStack()
-        }
-    }
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@Composable
-fun SinglePasskeyScreen(
     entry: CredentialEntryInfo,
     columnState: ScalingLazyColumnState,
     modifier: Modifier = Modifier,
-    viewModel: SinglePasskeyScreenViewModel,
+    flowEngine: FlowEngine,
 ) {
     SingleAccountScreen(
         headerContent = {
             SignInHeader(
                 icon = entry.icon,
-                title = stringResource(R.string.use_passkey_title),
+                title = stringResource(R.string.use_password_title),
             )
         },
         accountContent = {
-            if (entry.displayName != null) {
-                AccountRow(
+            AccountRow(
                     primaryText = checkNotNull(entry.displayName),
                     secondaryText = entry.userName,
                     modifier = Modifier.padding(top = 10.dp),
                 )
-            } else {
-                AccountRow(
-                    primaryText = entry.userName,
-                    modifier = Modifier.padding(top = 10.dp),
-                )
-            }
         },
         columnState = columnState,
         modifier = modifier.padding(horizontal = 10.dp)
     ) {
         item {
+            val selectEntry = flowEngine.getEntrySelector()
             Column {
-                ContinueChip(viewModel::onContinueClick)
-                SignInOptionsChip(viewModel::onSignInOptionsClick)
-                DismissChip(viewModel::onDismissClick)
+                ContinueChip { selectEntry(entry, false) }
+                SignInOptionsChip{ flowEngine.openSecondaryScreen() }
+                DismissChip { flowEngine.cancel() }
             }
         }
     }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
deleted file mode 100644
index 37ffaca..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.screens.single.passkey
-
-import android.content.Intent
-import android.credentials.selection.UserSelectionDialogResult
-import android.credentials.selection.ProviderPendingIntentResponse
-import androidx.annotation.MainThread
-import androidx.lifecycle.ViewModel
-import com.android.credentialmanager.ktx.getIntentSenderRequest
-import com.android.credentialmanager.model.Request
-import com.android.credentialmanager.client.CredentialManagerClient
-import com.android.credentialmanager.model.get.CredentialEntryInfo
-import dagger.hilt.android.lifecycle.HiltViewModel
-import com.android.credentialmanager.ui.screens.UiState
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import javax.inject.Inject
-
-@HiltViewModel
-class SinglePasskeyScreenViewModel @Inject constructor(
-    private val credentialManagerClient: CredentialManagerClient,
-) : ViewModel() {
-
-    private val _uiState =
-        MutableStateFlow<UiState>(UiState.CredentialScreen)
-    val uiState: StateFlow<UiState> = _uiState
-
-    private lateinit var requestGet: Request.Get
-    private lateinit var entryInfo: CredentialEntryInfo
-
-
-    @MainThread
-    fun initialize(entry: CredentialEntryInfo) {
-        this.entryInfo = entry
-    }
-
-    fun onDismissClick() {
-        _uiState.value = UiState.Cancel
-    }
-
-    fun onContinueClick() {
-        _uiState.value = UiState.CredentialSelected(
-            intentSenderRequest = entryInfo.getIntentSenderRequest()
-        )
-    }
-
-    fun onSignInOptionsClick() {
-    }
-
-    fun onPasskeyInfoRetrieved(
-        resultCode: Int? = null,
-        resultData: Intent? = null,
-    ) {
-        val userSelectionDialogResult = UserSelectionDialogResult(
-            requestGet.token,
-            entryInfo.providerId,
-            entryInfo.entryKey,
-            entryInfo.entrySubkey,
-            if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
-        )
-        credentialManagerClient.sendResult(userSelectionDialogResult)
-    }
-}
-
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
index b0ece0d..3a86feb 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
@@ -16,100 +16,43 @@
 
 package com.android.credentialmanager.ui.screens.single.signInWithProvider
 
-import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
-import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.R
-import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
 import com.android.credentialmanager.ui.components.AccountRow
 import com.android.credentialmanager.ui.components.ContinueChip
 import com.android.credentialmanager.ui.components.DismissChip
 import com.android.credentialmanager.ui.components.SignInHeader
 import com.android.credentialmanager.ui.components.SignInOptionsChip
 import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
-import com.android.credentialmanager.ui.screens.UiState
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import com.google.android.horologist.compose.layout.ScalingLazyColumnState
 
 /**
  * Screen that shows sign in with provider credential.
  *
- * @param credentialSelectorUiState The app bar view model.
+ * @param entry The password entry.
  * @param columnState ScalingLazyColumn configuration to be be applied to SingleAccountScreen
  * @param modifier styling for composable
- * @param viewModel ViewModel that updates ui state for this screen
- * @param navController handles navigation events from this screen
+ * @param flowEngine [FlowEngine] that updates ui state for this screen
  */
 @OptIn(ExperimentalHorologistApi::class)
 @Composable
 fun SignInWithProviderScreen(
-    credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry,
-    columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
-    viewModel: SignInWithProviderViewModel = hiltViewModel(),
-    navController: NavHostController = rememberNavController(),
-) {
-    viewModel.initialize(credentialSelectorUiState.entry)
-
-    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
-    when (uiState) {
-        UiState.CredentialScreen -> {
-            SignInWithProviderScreen(
-                credentialSelectorUiState.entry,
-                columnState,
-                modifier,
-                viewModel
-            )
-        }
-
-        is UiState.CredentialSelected -> {
-            val launcher = rememberLauncherForActivityResult(
-                StartBalIntentSenderForResultContract()
-            ) {
-                viewModel.onInfoRetrieved(it.resultCode, null)
-            }
-
-            SideEffect {
-                (uiState as UiState.CredentialSelected).intentSenderRequest?.let {
-                    launcher.launch(it)
-                }
-            }
-        }
-
-        UiState.Cancel -> {
-            // TODO(b/322797032) add valid navigation path here for going back
-            navController.popBackStack()
-        }
-    }
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@Composable
-fun SignInWithProviderScreen(
     entry: CredentialEntryInfo,
     columnState: ScalingLazyColumnState,
     modifier: Modifier = Modifier,
-    viewModel: SignInWithProviderViewModel,
+    flowEngine: FlowEngine,
 ) {
     SingleAccountScreen(
         headerContent = {
             SignInHeader(
                 icon = entry.icon,
-                title = stringResource(R.string.use_sign_in_with_provider_title,
-                    entry.providerDisplayName),
+                title = entry.providerDisplayName,
             )
         },
         accountContent = {
@@ -130,12 +73,13 @@
         columnState = columnState,
         modifier = modifier.padding(horizontal = 10.dp)
     ) {
-       item {
-           Column {
-               ContinueChip(viewModel::onContinueClick)
-               SignInOptionsChip(viewModel::onSignInOptionsClick)
-               DismissChip(viewModel::onDismissClick)
-           }
-       }
+        item {
+            val selectEntry = flowEngine.getEntrySelector()
+            Column {
+                ContinueChip { selectEntry(entry, false) }
+                SignInOptionsChip{ flowEngine.openSecondaryScreen() }
+                DismissChip { flowEngine.cancel() }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt
deleted file mode 100644
index 7ba45e5..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.screens.single.signInWithProvider
-
-import android.content.Intent
-import android.credentials.selection.ProviderPendingIntentResponse
-import android.credentials.selection.UserSelectionDialogResult
-import androidx.annotation.MainThread
-import androidx.lifecycle.ViewModel
-import com.android.credentialmanager.ktx.getIntentSenderRequest
-import com.android.credentialmanager.model.Request
-import com.android.credentialmanager.client.CredentialManagerClient
-import com.android.credentialmanager.model.get.CredentialEntryInfo
-import dagger.hilt.android.lifecycle.HiltViewModel
-import com.android.credentialmanager.ui.screens.UiState
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import javax.inject.Inject
-
-/** ViewModel for [SignInWithProviderScreen].*/
-@HiltViewModel
-class SignInWithProviderViewModel @Inject constructor(
-    private val credentialManagerClient: CredentialManagerClient,
-) : ViewModel() {
-
-    private val _uiState =
-        MutableStateFlow<UiState>(UiState.CredentialScreen)
-    val uiState: StateFlow<UiState> = _uiState
-
-    private lateinit var requestGet: Request.Get
-    private lateinit var entryInfo: CredentialEntryInfo
-
-    @MainThread
-    fun initialize(entry: CredentialEntryInfo) {
-        this.entryInfo = entry
-    }
-
-    fun onDismissClick() {
-        _uiState.value = UiState.Cancel
-    }
-
-    fun onContinueClick() {
-        _uiState.value = UiState.CredentialSelected(
-            intentSenderRequest = entryInfo.getIntentSenderRequest()
-        )
-    }
-
-    fun onSignInOptionsClick() {
-        // TODO(b/322797032) Implement navigation route for single credential screen to multiple
-        // credentials
-    }
-
-    fun onInfoRetrieved(
-        resultCode: Int? = null,
-        resultData: Intent? = null,
-    ) {
-        val userSelectionDialogResult = UserSelectionDialogResult(
-            requestGet.token,
-            entryInfo.providerId,
-            entryInfo.entryKey,
-            entryInfo.entrySubkey,
-            if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
-        )
-        credentialManagerClient.sendResult(userSelectionDialogResult)
-    }
-}
-
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
index 0e39493..cfdcaff 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
@@ -26,23 +26,10 @@
 
 /** Manager of [BackupRestoreStorage]. */
 class BackupRestoreStorageManager private constructor(private val application: Application) {
-    private val storages = ConcurrentHashMap<String, BackupRestoreStorage>()
+    private val storageWrappers = ConcurrentHashMap<String, StorageWrapper>()
 
     private val executor = MoreExecutors.directExecutor()
 
-    private val observer = Observer { reason -> notifyBackupManager(null, reason) }
-
-    private val keyedObserver =
-        KeyedObserver<Any?> { key, reason -> notifyBackupManager(key, reason) }
-
-    private fun notifyBackupManager(key: Any?, reason: Int) {
-        // prefer not triggering backup immediately after restore
-        if (reason == ChangeReason.RESTORE) return
-        // TODO: log storage name
-        Log.d(LOG_TAG, "Notify BackupManager data changed for change: key=$key")
-        BackupManager.dataChanged(application.packageName)
-    }
-
     /**
      * Adds all the registered [BackupRestoreStorage] as the helpers of given [BackupAgentHelper].
      *
@@ -52,7 +39,8 @@
      */
     fun addBackupAgentHelpers(backupAgentHelper: BackupAgentHelper) {
         val fileStorages = mutableListOf<BackupRestoreFileStorage>()
-        for ((keyPrefix, storage) in storages) {
+        for ((keyPrefix, storageWrapper) in storageWrappers) {
+            val storage = storageWrapper.storage
             if (storage is BackupRestoreFileStorage) {
                 fileStorages.add(storage)
             } else {
@@ -70,15 +58,8 @@
      * The observers of the storages will be notified.
      */
     fun onRestoreFinished() {
-        for (storage in storages.values) {
-            storage.notifyRestoreFinished()
-        }
-    }
-
-    private fun BackupRestoreStorage.notifyRestoreFinished() {
-        when (this) {
-            is KeyedObservable<*> -> notifyChange(ChangeReason.RESTORE)
-            is Observable -> notifyChange(ChangeReason.RESTORE)
+        for (storageWrapper in storageWrappers.values) {
+            storageWrapper.notifyRestoreFinished()
         }
     }
 
@@ -99,50 +80,82 @@
     fun add(storage: BackupRestoreStorage) {
         if (storage is BackupRestoreFileStorage) storage.checkFilePaths()
         val name = storage.name
-        val oldStorage = storages.put(name, storage)
+        val oldStorage = storageWrappers.put(name, StorageWrapper(storage))?.storage
         if (oldStorage != null) {
             throw IllegalStateException(
                 "Storage name '$name' conflicts:\n\told: $oldStorage\n\tnew: $storage"
             )
         }
-        storage.addObserver()
-    }
-
-    private fun BackupRestoreStorage.addObserver() {
-        when (this) {
-            is KeyedObservable<*> -> addObserver(keyedObserver, executor)
-            is Observable -> addObserver(observer, executor)
-            else ->
-                throw IllegalArgumentException(
-                    "$this does not implement either KeyedObservable or Observable"
-                )
-        }
     }
 
     /** Removes all the storages. */
     fun removeAll() {
-        for ((name, _) in storages) remove(name)
+        for ((name, _) in storageWrappers) remove(name)
     }
 
     /** Removes storage with given name. */
     fun remove(name: String): BackupRestoreStorage? {
-        val storage = storages.remove(name)
-        storage?.removeObserver()
-        return storage
-    }
-
-    private fun BackupRestoreStorage.removeObserver() {
-        when (this) {
-            is KeyedObservable<*> -> removeObserver(keyedObserver)
-            is Observable -> removeObserver(observer)
-        }
+        val storageWrapper = storageWrappers.remove(name)
+        storageWrapper?.removeObserver()
+        return storageWrapper?.storage
     }
 
     /** Returns storage with given name. */
-    fun get(name: String): BackupRestoreStorage? = storages[name]
+    fun get(name: String): BackupRestoreStorage? = storageWrappers[name]?.storage
 
     /** Returns storage with given name, exception is raised if not found. */
-    fun getOrThrow(name: String): BackupRestoreStorage = storages[name]!!
+    fun getOrThrow(name: String): BackupRestoreStorage = storageWrappers[name]!!.storage
+
+    private inner class StorageWrapper(val storage: BackupRestoreStorage) :
+        Observer, KeyedObserver<Any?> {
+        init {
+            when (storage) {
+                is KeyedObservable<*> -> storage.addObserver(this, executor)
+                is Observable -> storage.addObserver(this, executor)
+                else ->
+                    throw IllegalArgumentException(
+                        "$this does not implement either KeyedObservable or Observable"
+                    )
+            }
+        }
+
+        override fun onChanged(reason: Int) = onKeyChanged(null, reason)
+
+        override fun onKeyChanged(key: Any?, reason: Int) {
+            notifyBackupManager(key, reason)
+        }
+
+        private fun notifyBackupManager(key: Any?, reason: Int) {
+            val name = storage.name
+            // prefer not triggering backup immediately after restore
+            if (reason == ChangeReason.RESTORE) {
+                Log.d(
+                    LOG_TAG,
+                    "Notify BackupManager dataChanged ignored for restore: storage=$name key=$key"
+                )
+                return
+            }
+            Log.d(
+                LOG_TAG,
+                "Notify BackupManager dataChanged: storage=$name key=$key reason=$reason"
+            )
+            BackupManager.dataChanged(application.packageName)
+        }
+
+        fun removeObserver() {
+            when (storage) {
+                is KeyedObservable<*> -> storage.removeObserver(this)
+                is Observable -> storage.removeObserver(this)
+            }
+        }
+
+        fun notifyRestoreFinished() {
+            when (storage) {
+                is KeyedObservable<*> -> storage.notifyChange(ChangeReason.RESTORE)
+                is Observable -> storage.notifyChange(ChangeReason.RESTORE)
+            }
+        }
+    }
 
     companion object {
         @Volatile private var instance: BackupRestoreStorageManager? = null
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java
index 840c936..b7108c9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java
@@ -236,7 +236,8 @@
             // Handle specific carrier config values for the default data SIM
             int defaultDataSubId = SubscriptionManager.from(context)
                     .getDefaultDataSubscriptionId();
-            PersistableBundle b = configMgr.getConfigForSubId(defaultDataSubId);
+            PersistableBundle b = configMgr == null ? null
+                        : configMgr.getConfigForSubId(defaultDataSubId);
             if (b != null) {
                 config.alwaysShowDataRatIcon = b.getBoolean(
                         CarrierConfigManager.KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL);
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index 53daef1..69c7410 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -242,6 +242,7 @@
                         .setMessage(messageResId)
                         .setNegativeButtonText(R.string.cancel)
                         .setPositiveButtonText(R.string.next);
+                mCustomDialogHelper.requestFocusOnTitle();
                 break;
             case GRANT_ADMIN_DIALOG:
                 mEditUserInfoView.setVisibility(View.GONE);
@@ -254,6 +255,7 @@
                         .setMessage(R.string.user_grant_admin_message)
                         .setNegativeButtonText(R.string.back)
                         .setPositiveButtonText(R.string.next);
+                mCustomDialogHelper.requestFocusOnTitle();
                 if (mIsAdmin == null) {
                     mCustomDialogHelper.setButtonEnabled(false);
                 }
@@ -265,6 +267,7 @@
                         .setTitle(R.string.user_info_settings_title)
                         .setNegativeButtonText(R.string.back)
                         .setPositiveButtonText(R.string.done);
+                mCustomDialogHelper.requestFocusOnTitle();
                 mEditUserInfoView.setVisibility(View.VISIBLE);
                 mGrantAdminView.setVisibility(View.GONE);
                 break;
@@ -273,7 +276,6 @@
                         && mEditUserPhotoController.getNewUserPhotoDrawable() != null)
                         ? mEditUserPhotoController.getNewUserPhotoDrawable()
                         : mSavedDrawable;
-
                 String newName = mUserNameView.getText().toString().trim();
                 String defaultName = mActivity.getString(R.string.user_new_user_name);
                 mUserName = !newName.isEmpty() ? newName : defaultName;
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
index 5201b3d..4cf3bc2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
@@ -23,6 +23,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -282,4 +283,13 @@
         }
         return this;
     }
+
+    /**
+     * Requests focus on dialog title when used. Used to let talkback know that the dialog content
+     * is updated and needs to be read from the beginning.
+     */
+    public void requestFocusOnTitle() {
+        mDialogTitle.requestFocus();
+        mDialogTitle.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
index 56b0bf7..1586b8f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
@@ -23,8 +23,8 @@
 import com.android.settingslib.volume.shared.model.AudioStreamModel
 import com.android.settingslib.volume.shared.model.RingerMode
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 /** Provides audio stream state and an ability to change it */
@@ -43,6 +43,9 @@
             streamModel.copy(volume = processVolume(streamModel, ringerMode, isZenMuted))
         }
 
+    val ringerMode: StateFlow<RingerMode>
+        get() = audioRepository.ringerMode
+
     suspend fun setVolume(audioStream: AudioStream, volume: Int) =
         audioRepository.setVolume(audioStream, volume)
 
@@ -52,9 +55,14 @@
     /** Checks if the volume can be changed via the UI. */
     fun canChangeVolume(audioStream: AudioStream): Flow<Boolean> {
         return if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
-            getAudioStream(AudioStream(AudioManager.STREAM_RING)).map { !it.isMuted }
+            combine(
+                notificationsSoundPolicyInteractor.isZenMuted(audioStream),
+                getAudioStream(AudioStream(AudioManager.STREAM_RING)).map { it.isMuted },
+            ) { isZenMuted, isRingMuted ->
+                !isZenMuted && !isRingMuted
+            }
         } else {
-            flowOf(true)
+            notificationsSoundPolicyInteractor.isZenMuted(audioStream).map { !it }
         }
     }
 
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index da06830..85bdb29 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -617,3 +617,14 @@
     description: "enables new focus outline for qs tiles when focused on with physical keyboard"
     bug: "312899524"
 }
+
+flag {
+   name: "edgeback_gesture_handler_get_running_tasks_background"
+    namespace: "systemui"
+    description: "Decide whether to get the running tasks from activity manager in EdgebackGestureHandler"
+        " class on the background thread."
+    bug: "325041960"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
index 5d5f12e..3f57f88 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
@@ -337,6 +337,7 @@
         if (ghostedView is LaunchableView) {
             // Restore the ghosted view visibility.
             ghostedView.setShouldBlockVisibilityChanges(false)
+            ghostedView.onActivityLaunchAnimationEnd()
         } else {
             // Make the ghosted view visible. We ensure that the view is considered VISIBLE by
             // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
index ed8e705..da6ccaa 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
@@ -38,6 +38,9 @@
      * @param block whether we should block/postpone all calls to `setVisibility`.
      */
     fun setShouldBlockVisibilityChanges(block: Boolean)
+
+    /** Perform an action when the activity launch animation ends */
+    fun onActivityLaunchAnimationEnd() {}
 }
 
 /** A delegate that can be used by views to make the implementation of [LaunchableView] easier. */
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
index ef15c84..9a99649 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
@@ -21,22 +21,25 @@
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
 import androidx.compose.foundation.interaction.DragInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Slider
 import androidx.compose.material3.SliderState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -44,17 +47,28 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.RoundRect
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import com.android.compose.modifiers.padding
+import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastFirstOrNull
 import com.android.compose.theme.LocalAndroidColorScheme
 
 /**
@@ -62,15 +76,16 @@
  *
  * @param onValueChangeFinished is called when the slider settles on a [value]. This callback
  *   shouldn't be used to react to value changes. Use [onValueChange] instead
- * @param interactionSource - the [MutableInteractionSource] representing the stream of Interactions
+ * @param interactionSource the [MutableInteractionSource] representing the stream of Interactions
  *   for this slider. You can create and pass in your own remembered instance to observe
  *   Interactions and customize the appearance / behavior of this slider in different states.
- * @param colors - slider color scheme.
- * @param draggingCornersRadius - radius of the slider indicator when the user drags it
- * @param icon - icon at the start of the slider. Icon is limited to a square space at the start of
- *   the slider
- * @param label - control shown next to the icon.
+ * @param colors determine slider color scheme.
+ * @param draggingCornersRadius is the radius of the slider indicator when the user drags it
+ * @param icon at the start of the slider. Icon is limited to a square space at the start of the
+ *   slider
+ * @param label is shown next to the icon.
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun PlatformSlider(
     value: Float,
@@ -86,7 +101,7 @@
     label: (@Composable (isDragging: Boolean) -> Unit)? = null,
 ) {
     val sliderHeight: Dp = 64.dp
-    val iconWidth: Dp = sliderHeight
+    val thumbSize: Dp = sliderHeight
     var isDragging by remember { mutableStateOf(false) }
     LaunchedEffect(interactionSource) {
         interactionSource.interactions.collect { interaction ->
@@ -101,16 +116,6 @@
             }
         }
     }
-    val paddingStart by
-        animateDpAsState(
-            targetValue =
-                if ((!isDragging && value == valueRange.start) || icon == null) {
-                    16.dp
-                } else {
-                    0.dp
-                },
-            label = "LabelIconSpacingAnimation"
-        )
 
     Box(modifier = modifier.height(sliderHeight)) {
         Slider(
@@ -126,130 +131,275 @@
                     sliderState = it,
                     enabled = enabled,
                     colors = colors,
-                    iconWidth = iconWidth,
                     draggingCornersRadius = draggingCornersRadius,
                     sliderHeight = sliderHeight,
+                    thumbSize = thumbSize,
                     isDragging = isDragging,
-                    modifier = Modifier,
+                    label = label,
+                    icon = icon,
+                    modifier = Modifier.fillMaxSize(),
                 )
             },
-            thumb = { Spacer(Modifier.width(iconWidth).height(sliderHeight)) },
+            thumb = { Spacer(Modifier.size(thumbSize)) },
         )
 
-        if (icon != null || label != null) {
-            Row(modifier = Modifier.fillMaxSize()) {
-                icon?.let { iconComposable ->
-                    Box(
-                        modifier = Modifier.fillMaxHeight().aspectRatio(1f),
-                        contentAlignment = Alignment.Center,
-                    ) {
-                        iconComposable(isDragging)
-                    }
-                }
-
-                label?.let { labelComposable ->
-                    Box(
-                        modifier =
-                            Modifier.fillMaxHeight()
-                                .weight(1f)
-                                .padding(
-                                    start = { paddingStart.roundToPx() },
-                                    end = { sliderHeight.roundToPx() / 2 },
-                                ),
-                        contentAlignment = Alignment.CenterStart,
-                    ) {
-                        labelComposable(isDragging)
-                    }
-                }
-            }
-        }
+        Spacer(
+            Modifier.padding(8.dp)
+                .size(4.dp)
+                .align(Alignment.CenterEnd)
+                .background(color = colors.indicatorColor, shape = CircleShape)
+        )
     }
 }
 
+private enum class TrackComponent(val zIndex: Float) {
+    Background(0f),
+    Icon(1f),
+    Label(1f),
+}
+
 @Composable
 private fun Track(
     sliderState: SliderState,
     enabled: Boolean,
     colors: PlatformSliderColors,
-    iconWidth: Dp,
     draggingCornersRadius: Dp,
     sliderHeight: Dp,
+    thumbSize: Dp,
     isDragging: Boolean,
+    icon: (@Composable (isDragging: Boolean) -> Unit)?,
+    label: (@Composable (isDragging: Boolean) -> Unit)?,
     modifier: Modifier = Modifier,
 ) {
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-    val iconWidthPx: Float
-    val halfIconWidthPx: Float
-    val targetIndicatorRadiusPx: Float
-    val halfSliderHeightPx: Float
-    with(LocalDensity.current) {
-        halfSliderHeightPx = sliderHeight.toPx() / 2
-        iconWidthPx = iconWidth.toPx()
-        halfIconWidthPx = iconWidthPx / 2
-        targetIndicatorRadiusPx =
-            if (isDragging) draggingCornersRadius.toPx() else halfSliderHeightPx
-    }
+    var drawingState: DrawingState by remember { mutableStateOf(DrawingState()) }
+    Layout(
+        modifier = modifier,
+        content = {
+            TrackBackground(
+                modifier = Modifier.layoutId(TrackComponent.Background),
+                drawingState = drawingState,
+                enabled = enabled,
+                colors = colors,
+                draggingCornersRadiusActive = draggingCornersRadius,
+                draggingCornersRadiusIdle = sliderHeight / 2,
+                isDragging = isDragging,
+            )
+            if (icon != null) {
+                Box(
+                    modifier = Modifier.layoutId(TrackComponent.Icon).clip(CircleShape),
+                    contentAlignment = Alignment.Center,
+                ) {
+                    CompositionLocalProvider(
+                        LocalContentColor provides
+                            if (enabled) colors.iconColor else colors.disabledIconColor
+                    ) {
+                        icon(isDragging)
+                    }
+                }
+            }
+            if (label != null) {
+                val offsetX by
+                    animateFloatAsState(
+                        targetValue =
+                            if (enabled) {
+                                if (drawingState.isLabelOnTopOfIndicator) {
+                                    drawingState.iconWidth.coerceAtLeast(
+                                        LocalDensity.current.run { 16.dp.toPx() }
+                                    )
+                                } else {
+                                    val indicatorWidth =
+                                        drawingState.indicatorRight - drawingState.indicatorLeft
+                                    indicatorWidth + LocalDensity.current.run { 16.dp.toPx() }
+                                }
+                            } else {
+                                drawingState.iconWidth
+                            },
+                        label = "LabelIconSpacingAnimation"
+                    )
+                Box(
+                    modifier =
+                        Modifier.layoutId(TrackComponent.Label).offset {
+                            IntOffset(offsetX.toInt(), 0)
+                        },
+                    contentAlignment = Alignment.CenterStart,
+                ) {
+                    CompositionLocalProvider(
+                        LocalContentColor provides
+                            colors.getLabelColor(
+                                isEnabled = enabled,
+                                isLabelOnTopOfTheIndicator = drawingState.isLabelOnTopOfIndicator,
+                            )
+                    ) {
+                        label(isDragging)
+                    }
+                }
+            }
+        },
+        measurePolicy =
+            TrackMeasurePolicy(
+                sliderState = sliderState,
+                thumbSize = LocalDensity.current.run { thumbSize.roundToPx() },
+                isRtl = isRtl,
+                onDrawingStateMeasured = { drawingState = it }
+            )
+    )
+}
 
-    val indicatorRadiusPx: Float by
-        animateFloatAsState(
-            targetValue = targetIndicatorRadiusPx,
+@Composable
+private fun TrackBackground(
+    drawingState: DrawingState,
+    enabled: Boolean,
+    colors: PlatformSliderColors,
+    draggingCornersRadiusActive: Dp,
+    draggingCornersRadiusIdle: Dp,
+    isDragging: Boolean,
+    modifier: Modifier = Modifier,
+) {
+    val indicatorRadiusDp: Dp by
+        animateDpAsState(
+            targetValue =
+                if (isDragging) draggingCornersRadiusActive else draggingCornersRadiusIdle,
             label = "PlatformSliderCornersAnimation",
         )
 
     val trackColor = colors.getTrackColor(enabled)
     val indicatorColor = colors.getIndicatorColor(enabled)
-    val trackCornerRadius = CornerRadius(halfSliderHeightPx, halfSliderHeightPx)
-    val indicatorCornerRadius = CornerRadius(indicatorRadiusPx, indicatorRadiusPx)
     Canvas(modifier.fillMaxSize()) {
+        val trackCornerRadius = CornerRadius(size.height / 2, size.height / 2)
         val trackPath = Path()
         trackPath.addRoundRect(
             RoundRect(
-                left = -halfIconWidthPx,
+                left = 0f,
                 top = 0f,
-                right = size.width + halfIconWidthPx,
-                bottom = size.height,
+                right = drawingState.totalWidth,
+                bottom = drawingState.totalHeight,
                 cornerRadius = trackCornerRadius,
             )
         )
         drawPath(path = trackPath, color = trackColor)
 
+        val indicatorCornerRadius = CornerRadius(indicatorRadiusDp.toPx(), indicatorRadiusDp.toPx())
         clipPath(trackPath) {
             val indicatorPath = Path()
-            if (isRtl) {
-                indicatorPath.addRoundRect(
-                    RoundRect(
-                        left =
-                            size.width -
-                                size.width * sliderState.coercedNormalizedValue -
-                                halfIconWidthPx,
-                        top = 0f,
-                        right = size.width + iconWidthPx,
-                        bottom = size.height,
-                        topLeftCornerRadius = indicatorCornerRadius,
-                        topRightCornerRadius = trackCornerRadius,
-                        bottomRightCornerRadius = trackCornerRadius,
-                        bottomLeftCornerRadius = indicatorCornerRadius,
-                    )
+            indicatorPath.addRoundRect(
+                RoundRect(
+                    left = drawingState.indicatorLeft,
+                    top = drawingState.indicatorTop,
+                    right = drawingState.indicatorRight,
+                    bottom = drawingState.indicatorBottom,
+                    topLeftCornerRadius = trackCornerRadius,
+                    topRightCornerRadius = indicatorCornerRadius,
+                    bottomRightCornerRadius = indicatorCornerRadius,
+                    bottomLeftCornerRadius = trackCornerRadius,
                 )
-            } else {
-                indicatorPath.addRoundRect(
-                    RoundRect(
-                        left = -halfIconWidthPx,
-                        top = 0f,
-                        right = size.width * sliderState.coercedNormalizedValue + halfIconWidthPx,
-                        bottom = size.height,
-                        topLeftCornerRadius = trackCornerRadius,
-                        topRightCornerRadius = indicatorCornerRadius,
-                        bottomRightCornerRadius = indicatorCornerRadius,
-                        bottomLeftCornerRadius = trackCornerRadius,
-                    )
-                )
-            }
+            )
             drawPath(path = indicatorPath, color = indicatorColor)
         }
     }
 }
 
+/** Measures track components sizes and calls [onDrawingStateMeasured] when it's done. */
+private class TrackMeasurePolicy(
+    private val sliderState: SliderState,
+    private val thumbSize: Int,
+    private val isRtl: Boolean,
+    private val onDrawingStateMeasured: (DrawingState) -> Unit,
+) : MeasurePolicy {
+
+    override fun MeasureScope.measure(
+        measurables: List<Measurable>,
+        constraints: Constraints
+    ): MeasureResult {
+        // Slider adds a paddings to the Track to make spase for thumb
+        val desiredWidth = constraints.maxWidth + thumbSize
+        val desiredHeight = constraints.maxHeight
+        val backgroundPlaceable: Placeable =
+            measurables
+                .fastFirst { it.layoutId == TrackComponent.Background }
+                .measure(Constraints(desiredWidth, desiredWidth, desiredHeight, desiredHeight))
+
+        val iconPlaceable: Placeable? =
+            measurables
+                .fastFirstOrNull { it.layoutId == TrackComponent.Icon }
+                ?.measure(
+                    Constraints(
+                        minWidth = desiredHeight,
+                        maxWidth = desiredHeight,
+                        minHeight = desiredHeight,
+                        maxHeight = desiredHeight,
+                    )
+                )
+
+        val iconSize = iconPlaceable?.width ?: 0
+        val labelMaxWidth = (desiredWidth - iconSize) / 2
+        val labelPlaceable: Placeable? =
+            measurables
+                .fastFirstOrNull { it.layoutId == TrackComponent.Label }
+                ?.measure(
+                    Constraints(
+                        minWidth = 0,
+                        maxWidth = labelMaxWidth,
+                        minHeight = desiredHeight,
+                        maxHeight = desiredHeight,
+                    )
+                )
+
+        val drawingState =
+            if (isRtl) {
+                DrawingState(
+                    isRtl = true,
+                    totalWidth = desiredWidth.toFloat(),
+                    totalHeight = desiredHeight.toFloat(),
+                    indicatorLeft =
+                        (desiredWidth - iconSize) * (1 - sliderState.coercedNormalizedValue),
+                    indicatorTop = 0f,
+                    indicatorRight = desiredWidth.toFloat(),
+                    indicatorBottom = desiredHeight.toFloat(),
+                    iconWidth = iconSize.toFloat(),
+                    labelWidth = labelPlaceable?.width?.toFloat() ?: 0f,
+                )
+            } else {
+                DrawingState(
+                    isRtl = false,
+                    totalWidth = desiredWidth.toFloat(),
+                    totalHeight = desiredHeight.toFloat(),
+                    indicatorLeft = 0f,
+                    indicatorTop = 0f,
+                    indicatorRight =
+                        iconSize + (desiredWidth - iconSize) * sliderState.coercedNormalizedValue,
+                    indicatorBottom = desiredHeight.toFloat(),
+                    iconWidth = iconSize.toFloat(),
+                    labelWidth = labelPlaceable?.width?.toFloat() ?: 0f,
+                )
+            }
+
+        onDrawingStateMeasured(drawingState)
+
+        return layout(desiredWidth, desiredHeight) {
+            backgroundPlaceable.placeRelative(0, 0, TrackComponent.Background.zIndex)
+
+            iconPlaceable?.placeRelative(0, 0, TrackComponent.Icon.zIndex)
+            labelPlaceable?.placeRelative(0, 0, TrackComponent.Label.zIndex)
+        }
+    }
+}
+
+private data class DrawingState(
+    val isRtl: Boolean = false,
+    val totalWidth: Float = 0f,
+    val totalHeight: Float = 0f,
+    val indicatorLeft: Float = 0f,
+    val indicatorTop: Float = 0f,
+    val indicatorRight: Float = 0f,
+    val indicatorBottom: Float = 0f,
+    val iconWidth: Float = 0f,
+    val labelWidth: Float = 0f,
+)
+
+private val DrawingState.isLabelOnTopOfIndicator: Boolean
+    get() = labelWidth < indicatorRight - indicatorLeft - iconWidth
+
 /** [SliderState.value] normalized using [SliderState.valueRange]. The result belongs to [0, 1] */
 private val SliderState.coercedNormalizedValue: Float
     get() {
@@ -268,17 +418,19 @@
  * @param trackColor fills the track of the slider. This is a "background" of the slider
  * @param indicatorColor fills the slider from the start to the value
  * @param iconColor is the default icon color
- * @param labelColor is the default icon color
+ * @param labelColorOnIndicator is the label color for when it's shown on top of the indicator
+ * @param labelColorOnTrack is the label color for when it's shown on top of the track
  * @param disabledTrackColor is the [trackColor] when the PlatformSlider#enabled == false
  * @param disabledIndicatorColor is the [indicatorColor] when the PlatformSlider#enabled == false
  * @param disabledIconColor is the [iconColor] when the PlatformSlider#enabled == false
- * @param disabledLabelColor is the [labelColor] when the PlatformSlider#enabled == false
+ * @param disabledLabelColor is the label color when the PlatformSlider#enabled == false
  */
 data class PlatformSliderColors(
     val trackColor: Color,
     val indicatorColor: Color,
     val iconColor: Color,
-    val labelColor: Color,
+    val labelColorOnIndicator: Color,
+    val labelColorOnTrack: Color,
     val disabledTrackColor: Color,
     val disabledIndicatorColor: Color,
     val disabledIconColor: Color,
@@ -300,10 +452,11 @@
 @Composable
 private fun lightThemePlatformSliderColors() =
     PlatformSliderColors(
-        trackColor = MaterialTheme.colorScheme.tertiaryContainer,
-        indicatorColor = LocalAndroidColorScheme.current.tertiaryFixedDim,
-        iconColor = MaterialTheme.colorScheme.onTertiaryContainer,
-        labelColor = MaterialTheme.colorScheme.onTertiaryContainer,
+        trackColor = LocalAndroidColorScheme.current.tertiaryFixedDim,
+        indicatorColor = MaterialTheme.colorScheme.tertiary,
+        iconColor = MaterialTheme.colorScheme.onTertiary,
+        labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary,
+        labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed,
         disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
         disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest,
         disabledIconColor = MaterialTheme.colorScheme.outline,
@@ -314,10 +467,11 @@
 @Composable
 private fun darkThemePlatformSliderColors() =
     PlatformSliderColors(
-        trackColor = MaterialTheme.colorScheme.onTertiary,
-        indicatorColor = LocalAndroidColorScheme.current.onTertiaryFixedVariant,
+        trackColor = MaterialTheme.colorScheme.tertiary,
+        indicatorColor = MaterialTheme.colorScheme.tertiary,
         iconColor = MaterialTheme.colorScheme.onTertiaryContainer,
-        labelColor = MaterialTheme.colorScheme.onTertiaryContainer,
+        labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary,
+        labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed,
         disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
         disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest,
         disabledIconColor = MaterialTheme.colorScheme.outline,
@@ -329,3 +483,14 @@
 
 private fun PlatformSliderColors.getIndicatorColor(isEnabled: Boolean): Color =
     if (isEnabled) indicatorColor else disabledIndicatorColor
+
+private fun PlatformSliderColors.getLabelColor(
+    isEnabled: Boolean,
+    isLabelOnTopOfTheIndicator: Boolean
+): Color {
+    return if (isEnabled) {
+        if (isLabelOnTopOfTheIndicator) labelColorOnIndicator else labelColorOnTrack
+    } else {
+        disabledLabelColor
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index a3372e3..d0c4984 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -24,7 +24,6 @@
 import com.android.compose.animation.scene.updateSceneTransitionLayoutState
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.communal.ui.compose.extensions.allowGestures
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.res.R
@@ -84,7 +83,7 @@
 
     SceneTransitionLayout(
         state = sceneTransitionLayoutState,
-        modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed),
+        modifier = modifier.fillMaxSize(),
         swipeSourceDetector =
             FixedSizeEdgeDetector(
                 dimensionResource(id = R.dimen.communal_gesture_initiation_width)
@@ -93,9 +92,14 @@
         scene(
             CommunalScenes.Blank,
             userActions =
-                mapOf(
-                    Swipe(SwipeDirection.Left, fromSource = Edge.Right) to CommunalScenes.Communal
-                )
+                if (touchesAllowed) {
+                    mapOf(
+                        Swipe(SwipeDirection.Left, fromSource = Edge.Right) to
+                            CommunalScenes.Communal
+                    )
+                } else {
+                    emptyMap()
+                }
         ) {
             // This scene shows nothing only allowing for transitions to the communal scene.
             Box(modifier = Modifier.fillMaxSize())
@@ -104,7 +108,13 @@
         scene(
             CommunalScenes.Communal,
             userActions =
-                mapOf(Swipe(SwipeDirection.Right, fromSource = Edge.Left) to CommunalScenes.Blank),
+                if (touchesAllowed) {
+                    mapOf(
+                        Swipe(SwipeDirection.Right, fromSource = Edge.Left) to CommunalScenes.Blank
+                    )
+                } else {
+                    emptyMap()
+                },
         ) {
             CommunalScene(viewModel, dialogFactory, modifier = modifier)
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index b5499b7..bc4e555 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -18,13 +18,16 @@
 
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayout
 import com.android.compose.animation.scene.transitions
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
 import javax.inject.Inject
@@ -40,6 +43,7 @@
 constructor(
     private val viewModel: LockscreenContentViewModel,
     private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
+    private val clockInteractor: KeyguardClockInteractor,
 ) {
 
     private val sceneKeyByBlueprint: Map<ComposableLockscreenSceneBlueprint, SceneKey> by lazy {
@@ -55,6 +59,12 @@
     ) {
         val coroutineScope = rememberCoroutineScope()
         val blueprintId by viewModel.blueprintId(coroutineScope).collectAsState()
+        val view = LocalView.current
+        DisposableEffect(view) {
+            clockInteractor.clockEventController.registerListeners(view)
+
+            onDispose { clockInteractor.clockEventController.unregisterListeners() }
+        }
 
         // Switch smoothly between blueprints, any composable tagged with element() will be
         // transition-animated between any two blueprints, if they both display the same element.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 96520b2..7acb4d5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -19,61 +19,31 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import com.android.compose.animation.scene.Edge
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneScope
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.animateSceneFloatAsState
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
 import dagger.Lazy
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
 
 /** The lock screen scene shows when the device is locked. */
 @SysUISingleton
 class LockscreenScene
 @Inject
 constructor(
-    @Application private val applicationScope: CoroutineScope,
     viewModel: LockscreenSceneViewModel,
     private val lockscreenContent: Lazy<LockscreenContent>,
 ) : ComposableScene {
     override val key = Scenes.Lockscreen
 
     override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
-        combine(
-                viewModel.upDestinationSceneKey,
-                viewModel.leftDestinationSceneKey,
-                viewModel.downFromTopEdgeDestinationSceneKey,
-            ) { upKey, leftKey, downFromTopEdgeKey ->
-                destinationScenes(
-                    up = upKey,
-                    left = leftKey,
-                    downFromTopEdge = downFromTopEdgeKey,
-                )
-            }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.Eagerly,
-                initialValue =
-                    destinationScenes(
-                        up = viewModel.upDestinationSceneKey.value,
-                        left = viewModel.leftDestinationSceneKey.value,
-                        downFromTopEdge = viewModel.downFromTopEdgeDestinationSceneKey.value,
-                    )
-            )
+        viewModel.destinationScenes
 
     @Composable
     override fun SceneScope.Content(
@@ -84,22 +54,6 @@
             modifier = modifier,
         )
     }
-
-    private fun destinationScenes(
-        up: SceneKey?,
-        left: SceneKey?,
-        downFromTopEdge: SceneKey?,
-    ): Map<UserAction, UserActionResult> {
-        return buildMap {
-            up?.let { this[Swipe(SwipeDirection.Up)] = UserActionResult(up) }
-            left?.let { this[Swipe(SwipeDirection.Left)] = UserActionResult(left) }
-            downFromTopEdge?.let {
-                this[Swipe(fromSource = Edge.Top, direction = SwipeDirection.Down)] =
-                    UserActionResult(downFromTopEdge)
-            }
-            this[Swipe(direction = SwipeDirection.Down)] = UserActionResult(Scenes.Shade)
-        }
-    }
 }
 
 @Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
new file mode 100644
index 0000000..c5ff859
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.transitions
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.largeClockElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smallClockElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smartspaceElementKey
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.ClockFaceInTransition.Companion.CLOCK_IN_MILLIS
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.ClockFaceInTransition.Companion.CLOCK_IN_START_DELAY_MILLIS
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.ClockFaceOutTransition.Companion.CLOCK_OUT_MILLIS
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.SmartspaceMoveTransition.Companion.STATUS_AREA_MOVE_DOWN_MILLIS
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.SmartspaceMoveTransition.Companion.STATUS_AREA_MOVE_UP_MILLIS
+
+object ClockTransition {
+    val defaultClockTransitions = transitions {
+        from(ClockScenes.smallClockScene, to = ClockScenes.largeClockScene) {
+            transitioningToLargeClock()
+        }
+        from(ClockScenes.largeClockScene, to = ClockScenes.smallClockScene) {
+            transitioningToSmallClock()
+        }
+    }
+
+    private fun TransitionBuilder.transitioningToLargeClock() {
+        spec = tween(durationMillis = STATUS_AREA_MOVE_UP_MILLIS.toInt())
+        timestampRange(
+            startMillis = CLOCK_IN_START_DELAY_MILLIS.toInt(),
+            endMillis = (CLOCK_IN_START_DELAY_MILLIS + CLOCK_IN_MILLIS).toInt()
+        ) {
+            fade(largeClockElementKey)
+        }
+
+        timestampRange(endMillis = CLOCK_OUT_MILLIS.toInt()) { fade(smallClockElementKey) }
+        anchoredTranslate(smallClockElementKey, smartspaceElementKey)
+    }
+
+    private fun TransitionBuilder.transitioningToSmallClock() {
+        spec = tween(durationMillis = STATUS_AREA_MOVE_DOWN_MILLIS.toInt())
+        timestampRange(
+            startMillis = CLOCK_IN_START_DELAY_MILLIS.toInt(),
+            endMillis = (CLOCK_IN_START_DELAY_MILLIS + CLOCK_IN_MILLIS).toInt()
+        ) {
+            fade(smallClockElementKey)
+        }
+
+        timestampRange(endMillis = CLOCK_OUT_MILLIS.toInt()) { fade(largeClockElementKey) }
+        anchoredTranslate(smallClockElementKey, smartspaceElementKey)
+    }
+}
+
+object ClockScenes {
+    val smallClockScene = SceneKey("small-clock-scene")
+    val largeClockScene = SceneKey("large-clock-scene")
+}
+
+object ClockElementKeys {
+    val largeClockElementKey = ElementKey("large-clock")
+    val smallClockElementKey = ElementKey("small-clock")
+    val smartspaceElementKey = ElementKey("smart-space")
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index d23cd0c..9509fd2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -17,19 +17,14 @@
 package com.android.systemui.keyguard.ui.composable.blueprint
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
-import com.android.compose.modifiers.padding
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -38,11 +33,8 @@
 import com.android.systemui.keyguard.ui.composable.section.MediaCarouselSection
 import com.android.systemui.keyguard.ui.composable.section.NotificationSection
 import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
-import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
 import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import com.android.systemui.media.controls.ui.composable.MediaCarousel
-import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoSet
@@ -59,14 +51,12 @@
     private val viewModel: LockscreenContentViewModel,
     private val statusBarSection: StatusBarSection,
     private val clockSection: DefaultClockSection,
-    private val smartSpaceSection: SmartSpaceSection,
     private val notificationSection: NotificationSection,
     private val lockSection: LockSection,
     private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
     private val bottomAreaSection: BottomAreaSection,
     private val settingsMenuSection: SettingsMenuSection,
     private val mediaCarouselSection: MediaCarouselSection,
-    private val clockInteractor: KeyguardClockInteractor,
 ) : ComposableLockscreenSceneBlueprint {
 
     override val id: String = "default"
@@ -74,7 +64,6 @@
     @Composable
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
-        val burnIn = rememberBurnIn(clockInteractor)
         val resources = LocalContext.current.resources
 
         LockscreenLongPress(
@@ -88,40 +77,7 @@
                         modifier = Modifier.fillMaxWidth(),
                     ) {
                         with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
-                        with(clockSection) {
-                            SmallClock(
-                                burnInParams = burnIn.parameters,
-                                onTopChanged = burnIn.onSmallClockTopChanged,
-                                modifier = Modifier.fillMaxWidth(),
-                            )
-                        }
-                        with(smartSpaceSection) {
-                            SmartSpace(
-                                burnInParams = burnIn.parameters,
-                                onTopChanged = burnIn.onSmartspaceTopChanged,
-                                modifier =
-                                    Modifier.fillMaxWidth()
-                                        .padding(
-                                            top = { viewModel.getSmartSpacePaddingTop(resources) },
-                                        )
-                                        .padding(
-                                            bottom =
-                                                dimensionResource(
-                                                    R.dimen.keyguard_status_view_bottom_margin
-                                                ),
-                                        ),
-                            )
-                        }
-
-                        if (viewModel.isLargeClockVisible) {
-                            Spacer(modifier = Modifier.weight(weight = 1f))
-                            with(clockSection) {
-                                LargeClock(
-                                    modifier = Modifier.fillMaxWidth(),
-                                )
-                            }
-                        }
-
+                        with(clockSection) { DefaultClockLayout() }
                         with(mediaCarouselSection) { MediaCarousel() }
 
                         if (viewModel.areNotificationsVisible(resources = resources)) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index c422c4b..9abfa42 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -17,19 +17,14 @@
 package com.android.systemui.keyguard.ui.composable.blueprint
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
-import com.android.compose.modifiers.padding
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -38,10 +33,8 @@
 import com.android.systemui.keyguard.ui.composable.section.MediaCarouselSection
 import com.android.systemui.keyguard.ui.composable.section.NotificationSection
 import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
-import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
 import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoSet
@@ -58,14 +51,12 @@
     private val viewModel: LockscreenContentViewModel,
     private val statusBarSection: StatusBarSection,
     private val clockSection: DefaultClockSection,
-    private val smartSpaceSection: SmartSpaceSection,
     private val notificationSection: NotificationSection,
     private val lockSection: LockSection,
     private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
     private val bottomAreaSection: BottomAreaSection,
     private val settingsMenuSection: SettingsMenuSection,
     private val mediaCarouselSection: MediaCarouselSection,
-    private val clockInteractor: KeyguardClockInteractor,
 ) : ComposableLockscreenSceneBlueprint {
 
     override val id: String = "shortcuts-besides-udfps"
@@ -73,7 +64,6 @@
     @Composable
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
-        val burnIn = rememberBurnIn(clockInteractor)
         val resources = LocalContext.current.resources
 
         LockscreenLongPress(
@@ -87,36 +77,7 @@
                         modifier = Modifier.fillMaxWidth(),
                     ) {
                         with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
-                        with(clockSection) {
-                            SmallClock(
-                                onTopChanged = burnIn.onSmallClockTopChanged,
-                                modifier = Modifier.fillMaxWidth(),
-                                burnInParams = burnIn.parameters,
-                            )
-                        }
-                        with(smartSpaceSection) {
-                            SmartSpace(
-                                burnInParams = burnIn.parameters,
-                                onTopChanged = burnIn.onSmartspaceTopChanged,
-                                modifier =
-                                    Modifier.fillMaxWidth()
-                                        .padding(
-                                            top = { viewModel.getSmartSpacePaddingTop(resources) }
-                                        )
-                                        .padding(
-                                            bottom =
-                                                dimensionResource(
-                                                    R.dimen.keyguard_status_view_bottom_margin
-                                                )
-                                        ),
-                            )
-                        }
-
-                        if (viewModel.isLargeClockVisible) {
-                            Spacer(modifier = Modifier.weight(weight = 1f))
-                            with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
-                        }
-
+                        with(clockSection) { DefaultClockLayout() }
                         with(mediaCarouselSection) { MediaCarousel() }
 
                         if (viewModel.areNotificationsVisible(resources = resources)) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
index d218425..652412d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -27,7 +26,6 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntRect
@@ -35,7 +33,6 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.padding
 import com.android.systemui.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -44,7 +41,6 @@
 import com.android.systemui.keyguard.ui.composable.section.MediaCarouselSection
 import com.android.systemui.keyguard.ui.composable.section.NotificationSection
 import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
-import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
 import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
 import com.android.systemui.res.R
@@ -65,14 +61,12 @@
     private val viewModel: LockscreenContentViewModel,
     private val statusBarSection: StatusBarSection,
     private val clockSection: DefaultClockSection,
-    private val smartSpaceSection: SmartSpaceSection,
     private val notificationSection: NotificationSection,
     private val lockSection: LockSection,
     private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
     private val bottomAreaSection: BottomAreaSection,
     private val settingsMenuSection: SettingsMenuSection,
     private val mediaCarouselSection: MediaCarouselSection,
-    private val clockInteractor: KeyguardClockInteractor,
     private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
 ) : ComposableLockscreenSceneBlueprint {
 
@@ -81,8 +75,6 @@
     @Composable
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
-        val burnIn = rememberBurnIn(clockInteractor)
-        val resources = LocalContext.current.resources
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -102,41 +94,7 @@
                                 modifier = Modifier.fillMaxHeight().weight(weight = 1f),
                                 horizontalAlignment = Alignment.CenterHorizontally,
                             ) {
-                                with(clockSection) {
-                                    SmallClock(
-                                        burnInParams = burnIn.parameters,
-                                        onTopChanged = burnIn.onSmallClockTopChanged,
-                                        modifier = Modifier.fillMaxWidth(),
-                                    )
-                                }
-
-                                with(smartSpaceSection) {
-                                    SmartSpace(
-                                        burnInParams = burnIn.parameters,
-                                        onTopChanged = burnIn.onSmartspaceTopChanged,
-                                        modifier =
-                                            Modifier.fillMaxWidth()
-                                                .padding(
-                                                    top = {
-                                                        viewModel.getSmartSpacePaddingTop(resources)
-                                                    },
-                                                )
-                                                .padding(
-                                                    bottom =
-                                                        dimensionResource(
-                                                            R.dimen
-                                                                .keyguard_status_view_bottom_margin
-                                                        )
-                                                ),
-                                    )
-                                }
-
-                                if (viewModel.isLargeClockVisible) {
-                                    Spacer(modifier = Modifier.weight(weight = 1f))
-                                    with(clockSection) { LargeClock() }
-                                    Spacer(modifier = Modifier.weight(weight = 1f))
-                                }
-
+                                with(clockSection) { DefaultClockLayout() }
                                 with(mediaCarouselSection) { MediaCarousel() }
                             }
                             with(notificationSection) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 152cc67..1ab2bc76 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -18,27 +18,37 @@
 
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.viewinterop.AndroidView
-import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayout
 import com.android.compose.modifiers.padding
-import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.customization.R as customizationR
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.largeClockElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smallClockElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smartspaceElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockTransition.defaultClockTransitions
+import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
 import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
 import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import com.android.systemui.res.R
 import javax.inject.Inject
 
 /** Provides small clock and large clock composables for the default clock face. */
@@ -48,112 +58,152 @@
     private val viewModel: KeyguardClockViewModel,
     private val clockInteractor: KeyguardClockInteractor,
     private val aodBurnInViewModel: AodBurnInViewModel,
+    private val lockscreenContentViewModel: LockscreenContentViewModel,
+    private val smartSpaceSection: SmartSpaceSection,
 ) {
+    @Composable
+    fun DefaultClockLayout(
+        modifier: Modifier = Modifier,
+    ) {
+        val isLargeClockVisible by viewModel.isLargeClockVisible.collectAsState()
+        val burnIn = rememberBurnIn(clockInteractor)
+        val currentScene =
+            if (isLargeClockVisible) {
+                largeClockScene
+            } else {
+                smallClockScene
+            }
+
+        LaunchedEffect(isLargeClockVisible) {
+            if (isLargeClockVisible) {
+                burnIn.onSmallClockTopChanged(null)
+            }
+        }
+
+        SceneTransitionLayout(
+            modifier = modifier,
+            currentScene = currentScene,
+            onChangeScene = {},
+            transitions = defaultClockTransitions,
+        ) {
+            scene(smallClockScene) {
+                Column {
+                    SmallClock(
+                        burnInParams = burnIn.parameters,
+                        onTopChanged = burnIn.onSmallClockTopChanged,
+                        modifier = Modifier.element(smallClockElementKey).fillMaxWidth()
+                    )
+                    SmartSpaceContent()
+                }
+            }
+
+            scene(largeClockScene) {
+                Column {
+                    SmartSpaceContent()
+                    LargeClock(modifier = Modifier.element(largeClockElementKey).fillMaxWidth())
+                }
+            }
+        }
+    }
 
     @Composable
-    fun SceneScope.SmallClock(
+    private fun SceneScope.SmartSpaceContent(
+        modifier: Modifier = Modifier,
+    ) {
+        val burnIn = rememberBurnIn(clockInteractor)
+        val resources = LocalContext.current.resources
+
+        MovableElement(key = smartspaceElementKey, modifier = modifier) {
+            content {
+                with(smartSpaceSection) {
+                    this@SmartSpaceContent.SmartSpace(
+                        burnInParams = burnIn.parameters,
+                        onTopChanged = burnIn.onSmartspaceTopChanged,
+                        modifier =
+                            Modifier.fillMaxWidth()
+                                .padding(
+                                    top = {
+                                        lockscreenContentViewModel.getSmartSpacePaddingTop(
+                                            resources
+                                        )
+                                    },
+                                    bottom = {
+                                        resources.getDimensionPixelSize(
+                                            R.dimen.keyguard_status_view_bottom_margin
+                                        )
+                                    }
+                                ),
+                    )
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun SceneScope.SmallClock(
         burnInParams: BurnInParameters,
         onTopChanged: (top: Float?) -> Unit,
         modifier: Modifier = Modifier,
     ) {
-        val clockSize by viewModel.clockSize.collectAsState()
         val currentClock by viewModel.currentClock.collectAsState()
-        viewModel.clock = currentClock
-
-        if (clockSize != KeyguardClockSwitch.SMALL || currentClock?.smallClock?.view == null) {
-            onTopChanged(null)
+        if (currentClock?.smallClock?.view == null) {
             return
         }
+        viewModel.clock = currentClock
 
-        val view = LocalView.current
+        val context = LocalContext.current
 
-        DisposableEffect(view) {
-            clockInteractor.clockEventController.registerListeners(view)
-
-            onDispose { clockInteractor.clockEventController.unregisterListeners() }
-        }
-
-        MovableElement(
-            key = ClockElementKey,
-            modifier = modifier,
-        ) {
-            content {
-                AndroidView(
-                    factory = { context ->
-                        FrameLayout(context).apply {
-                            val newClockView = checkNotNull(currentClock).smallClock.view
-                            (newClockView.parent as? ViewGroup)?.removeView(newClockView)
-                            addView(newClockView)
-                        }
-                    },
-                    modifier =
-                        Modifier.padding(
-                                horizontal =
-                                    dimensionResource(customizationR.dimen.clock_padding_start)
-                            )
-                            .padding(top = { viewModel.getSmallClockTopMargin(view.context) })
-                            .onTopPlacementChanged(onTopChanged)
-                            .burnInAware(
-                                viewModel = aodBurnInViewModel,
-                                params = burnInParams,
-                            ),
-                    update = {
-                        val newClockView = checkNotNull(currentClock).smallClock.view
-                        it.removeAllViews()
-                        (newClockView.parent as? ViewGroup)?.removeView(newClockView)
-                        it.addView(newClockView)
-                    },
-                )
-            }
-        }
+        AndroidView(
+            factory = { context ->
+                FrameLayout(context).apply {
+                    val newClockView = checkNotNull(currentClock).smallClock.view
+                    (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+                    addView(newClockView)
+                }
+            },
+            update = {
+                val newClockView = checkNotNull(currentClock).smallClock.view
+                it.removeAllViews()
+                (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+                it.addView(newClockView)
+            },
+            modifier =
+                modifier
+                    .padding(
+                        horizontal = dimensionResource(customizationR.dimen.clock_padding_start)
+                    )
+                    .padding(top = { viewModel.getSmallClockTopMargin(context) })
+                    .onTopPlacementChanged(onTopChanged)
+                    .burnInAware(
+                        viewModel = aodBurnInViewModel,
+                        params = burnInParams,
+                    ),
+        )
     }
 
     @Composable
-    fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
-        val clockSize by viewModel.clockSize.collectAsState()
+    private fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
         val currentClock by viewModel.currentClock.collectAsState()
         viewModel.clock = currentClock
-
-        if (clockSize != KeyguardClockSwitch.LARGE) {
-            return
-        }
-
         if (currentClock?.largeClock?.view == null) {
             return
         }
 
-        val view = LocalView.current
-
-        DisposableEffect(view) {
-            clockInteractor.clockEventController.registerListeners(view)
-
-            onDispose { clockInteractor.clockEventController.unregisterListeners() }
-        }
-
-        MovableElement(
-            key = ClockElementKey,
-            modifier = modifier,
-        ) {
-            content {
-                AndroidView(
-                    factory = { context ->
-                        FrameLayout(context).apply {
-                            val newClockView = checkNotNull(currentClock).largeClock.view
-                            (newClockView.parent as? ViewGroup)?.removeView(newClockView)
-                            addView(newClockView)
-                        }
-                    },
-                    update = {
-                        val newClockView = checkNotNull(currentClock).largeClock.view
-                        it.removeAllViews()
-                        (newClockView.parent as? ViewGroup)?.removeView(newClockView)
-                        it.addView(newClockView)
-                    },
-                    modifier = Modifier.fillMaxSize()
-                )
-            }
-        }
+        AndroidView(
+            factory = { context ->
+                FrameLayout(context).apply {
+                    val newClockView = checkNotNull(currentClock).largeClock.view
+                    (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+                    addView(newClockView)
+                }
+            },
+            update = {
+                val newClockView = checkNotNull(currentClock).largeClock.view
+                it.removeAllViews()
+                (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+                it.addView(newClockView)
+            },
+            modifier = modifier.fillMaxSize()
+        )
     }
 }
-
-private val ClockElementKey = ElementKey("Clock")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
index 9b71844..d1cc53e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
@@ -33,7 +33,6 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
-import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
@@ -60,7 +59,7 @@
         modifier: Modifier = Modifier,
     ) {
         Column(
-            modifier = modifier.element(SmartSpaceElementKey).onTopPlacementChanged(onTopChanged),
+            modifier = modifier.onTopPlacementChanged(onTopChanged),
         ) {
             if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) {
                 return
@@ -192,5 +191,3 @@
         )
     }
 }
-
-private val SmartSpaceElementKey = ElementKey("SmartSpace")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 0b9f503..51464d0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -73,6 +73,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
@@ -152,27 +153,29 @@
     mediaHost: MediaHost,
     modifier: Modifier = Modifier,
 ) {
-    val isSplitShade by viewModel.isSplitShade.collectAsState()
-    if (isSplitShade) {
-        SplitShade(
-            viewModel = viewModel,
-            createTintedIconManager = createTintedIconManager,
-            createBatteryMeterViewController = createBatteryMeterViewController,
-            statusBarIconController = statusBarIconController,
-            mediaCarouselController = mediaCarouselController,
-            mediaHost = mediaHost,
-            modifier = modifier,
-        )
-    } else {
-        SingleShade(
-            viewModel = viewModel,
-            createTintedIconManager = createTintedIconManager,
-            createBatteryMeterViewController = createBatteryMeterViewController,
-            statusBarIconController = statusBarIconController,
-            mediaCarouselController = mediaCarouselController,
-            mediaHost = mediaHost,
-            modifier = modifier,
-        )
+    val shadeMode by viewModel.shadeMode.collectAsState()
+    when (shadeMode) {
+        is ShadeMode.Single ->
+            SingleShade(
+                viewModel = viewModel,
+                createTintedIconManager = createTintedIconManager,
+                createBatteryMeterViewController = createBatteryMeterViewController,
+                statusBarIconController = statusBarIconController,
+                mediaCarouselController = mediaCarouselController,
+                mediaHost = mediaHost,
+                modifier = modifier,
+            )
+        is ShadeMode.Split ->
+            SplitShade(
+                viewModel = viewModel,
+                createTintedIconManager = createTintedIconManager,
+                createBatteryMeterViewController = createBatteryMeterViewController,
+                statusBarIconController = statusBarIconController,
+                mediaCarouselController = mediaCarouselController,
+                mediaHost = mediaHost,
+                modifier = modifier,
+            )
+        is ShadeMode.Dual -> error("Dual shade is not yet implemented!")
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt
index 8ac84ff..b1fbe35 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.volume.panel.component.anc.ui.composable
 
 import android.content.Context
+import android.view.ContextThemeWrapper
 import android.view.View
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.material3.MaterialTheme
@@ -73,15 +74,16 @@
         AndroidView<SliceView>(
             modifier = Modifier.fillMaxWidth(),
             factory = { context: Context ->
-                SliceView(context).apply {
-                    mode = SliceView.MODE_LARGE
-                    isScrollable = false
-                    importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
-                    setShowTitleItems(true)
-                    addOnLayoutChangeListener(
-                        OnWidthChangedLayoutListener(viewModel::changeSliceWidth)
-                    )
-                }
+                SliceView(ContextThemeWrapper(context, R.style.Widget_SliceView_VolumePanel))
+                    .apply {
+                        mode = SliceView.MODE_LARGE
+                        isScrollable = false
+                        importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+                        setShowTitleItems(true)
+                        addOnLayoutChangeListener(
+                            OnWidthChangedLayoutListener(viewModel::changeSliceWidth)
+                        )
+                    }
             },
             update = { sliceView: SliceView -> sliceView.slice = slice }
         )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt
index 4d810df..81d2da0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt
@@ -42,9 +42,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.painterResource
@@ -61,12 +58,13 @@
 @Composable
 fun ColumnVolumeSliders(
     viewModels: List<SliderViewModel>,
+    isExpanded: Boolean,
+    onExpandedChanged: (Boolean) -> Unit,
     sliderColors: PlatformSliderColors,
     isExpandable: Boolean,
     modifier: Modifier = Modifier,
 ) {
     require(viewModels.isNotEmpty())
-    var isExpanded: Boolean by remember(isExpandable) { mutableStateOf(!isExpandable) }
     val transition = updateTransition(isExpanded, label = "CollapsableSliders")
     Column(modifier = modifier) {
         Row(
@@ -85,7 +83,7 @@
             if (isExpandable) {
                 ExpandButton(
                     isExpanded = isExpanded,
-                    onExpandedChanged = { isExpanded = it },
+                    onExpandedChanged = onExpandedChanged,
                     sliderColors,
                     Modifier,
                 )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index 18a62dc..3e0aee5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -22,6 +22,7 @@
 import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -30,6 +31,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
 import com.android.compose.PlatformSlider
 import com.android.compose.PlatformSliderColors
 import com.android.systemui.common.ui.compose.Icon
@@ -54,7 +56,7 @@
             if (isDragging) {
                 Text(text = value.toInt().toString())
             } else {
-                state.icon?.let { Icon(icon = it) }
+                state.icon?.let { Icon(modifier = Modifier.size(24.dp), icon = it) }
             }
         },
         colors = sliderColors,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt
index 1ca18de..fdf8ee8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt
@@ -48,8 +48,11 @@
                 modifier = modifier.fillMaxWidth(),
             )
         } else {
+            val isExpanded by viewModel.isExpanded.collectAsState()
             ColumnVolumeSliders(
                 viewModels = sliderViewModels,
+                isExpanded = isExpanded,
+                onExpandedChanged = viewModel::onExpandedChanged,
                 sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(),
                 isExpandable = isPortrait,
                 modifier = modifier.fillMaxWidth(),
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 6114499..63ec54f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
+import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 import kotlin.math.absoluteValue
 import kotlinx.coroutines.CoroutineScope
@@ -353,10 +354,7 @@
 
         // If the swipe was not committed or if the swipe distance is not computed yet, don't do
         // anything.
-        if (
-            swipeTransition._currentScene != toScene ||
-                distance == SwipeTransition.DistanceUnspecified
-        ) {
+        if (swipeTransition._currentScene != toScene || distance == DistanceUnspecified) {
             return fromScene to 0f
         }
 
@@ -418,7 +416,7 @@
             var targetScene: Scene
             var targetOffset: Float
             if (
-                distance != SwipeTransition.DistanceUnspecified &&
+                distance != DistanceUnspecified &&
                     shouldCommitSwipe(
                         offset,
                         distance,
@@ -444,8 +442,8 @@
                     if (targetScene == fromScene) {
                         0f
                     } else {
-                        check(distance != SwipeTransition.DistanceUnspecified) {
-                            "distance is equal to ${SwipeTransition.DistanceUnspecified}"
+                        check(distance != DistanceUnspecified) {
+                            "distance is equal to $DistanceUnspecified"
                         }
                         distance
                     }
@@ -628,6 +626,12 @@
     /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
     lateinit var swipeSpec: SpringSpec<Float>
 
+    override val overscrollScope: OverscrollScope =
+        object : OverscrollScope {
+            override val absoluteDistance: Float
+                get() = distance().absoluteValue
+        }
+
     private var lastDistance = DistanceUnspecified
 
     /** Whether [TransitionState.Transition.finish] was called on this transition. */
@@ -753,10 +757,6 @@
         /** The job in which [animatable] is animated. */
         val job: Job,
     )
-
-    companion object {
-        const val DistanceUnspecified = 0f
-    }
 }
 
 private object DefaultSwipeDistance : UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 86124df..e6f5d58 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -236,19 +236,28 @@
 
     interface HasOverscrollProperties {
         /**
-         * The position of the [TransitionState.Transition.toScene].
+         * The position of the [Transition.toScene].
          *
          * Used to understand the direction of the overscroll.
          */
         val isUpOrLeft: Boolean
 
         /**
-         * The relative orientation between [TransitionState.Transition.fromScene] and
-         * [TransitionState.Transition.toScene].
+         * The relative orientation between [Transition.fromScene] and [Transition.toScene].
          *
          * Used to understand the orientation of the overscroll.
          */
         val orientation: Orientation
+
+        /**
+         * Scope which can be used in the Overscroll DSL to define a transformation based on the
+         * distance between [Transition.fromScene] and [Transition.toScene].
+         */
+        val overscrollScope: OverscrollScope
+
+        companion object {
+            const val DistanceUnspecified = 0f
+        }
     }
 }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 2dd41cd..b466143 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -30,6 +30,7 @@
 import com.android.compose.animation.scene.transformation.DrawScale
 import com.android.compose.animation.scene.transformation.EdgeTranslate
 import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.OverscrollTranslate
 import com.android.compose.animation.scene.transformation.PropertyTransformation
 import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
 import com.android.compose.animation.scene.transformation.ScaleSize
@@ -124,7 +125,7 @@
         overscrollSpecs.fastForEach { spec ->
             if (spec.orientation == orientation && filter(spec)) {
                 if (match != null) {
-                    error("Found multiple transition specs for transition $scene")
+                    error("Found multiple overscroll specs for overscroll $scene")
                 }
                 match = spec
             }
@@ -297,6 +298,7 @@
         ) {
             when (current) {
                 is Translate,
+                is OverscrollTranslate,
                 is EdgeTranslate,
                 is AnchoredTranslate -> {
                     throwIfNotNull(offset, element, name = "offset")
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index bc52a28..2c109a3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -22,6 +22,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
 
 /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
 fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
@@ -88,8 +89,7 @@
     ): OverscrollSpec
 }
 
-@TransitionDsl
-interface OverscrollBuilder : PropertyTransformationBuilder {
+interface BaseTransitionBuilder : PropertyTransformationBuilder {
     /**
      * The distance it takes for this transition to animate from 0% to 100% when it is driven by a
      * [UserAction].
@@ -120,7 +120,7 @@
 }
 
 @TransitionDsl
-interface TransitionBuilder : OverscrollBuilder, PropertyTransformationBuilder {
+interface TransitionBuilder : BaseTransitionBuilder {
     /**
      * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
      * the transition is triggered (i.e. it is not gesture-based).
@@ -176,6 +176,24 @@
     fun reversed(builder: TransitionBuilder.() -> Unit)
 }
 
+@TransitionDsl
+interface OverscrollBuilder : BaseTransitionBuilder {
+    /** Translate the element(s) matching [matcher] by ([x], [y]) pixels. */
+    fun translate(
+        matcher: ElementMatcher,
+        x: OverscrollScope.() -> Float = { 0f },
+        y: OverscrollScope.() -> Float = { 0f },
+    )
+}
+
+interface OverscrollScope {
+    /**
+     * Return the absolute distance between fromScene and toScene, if available, otherwise
+     * [DistanceUnspecified].
+     */
+    val absoluteDistance: Float
+}
+
 /**
  * An interface to decide where we should draw shared Elements or compose MovableElements.
  *
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 65e8ea5..1c9080f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -31,6 +31,7 @@
 import com.android.compose.animation.scene.transformation.DrawScale
 import com.android.compose.animation.scene.transformation.EdgeTranslate
 import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.OverscrollTranslate
 import com.android.compose.animation.scene.transformation.PropertyTransformation
 import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
 import com.android.compose.animation.scene.transformation.ScaleSize
@@ -114,7 +115,7 @@
     }
 }
 
-internal open class OverscrollBuilderImpl : OverscrollBuilder {
+internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder {
     val transformations = mutableListOf<Transformation>()
     private var range: TransformationRange? = null
     protected var reversed = false
@@ -130,7 +131,7 @@
         range = null
     }
 
-    private fun transformation(transformation: PropertyTransformation<*>) {
+    protected fun transformation(transformation: PropertyTransformation<*>) {
         val transformation =
             if (range != null) {
                 RangedPropertyTransformation(transformation, range!!)
@@ -185,7 +186,7 @@
     }
 }
 
-internal class TransitionBuilderImpl : OverscrollBuilderImpl(), TransitionBuilder {
+internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBuilder {
     override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
     override var swipeSpec: SpringSpec<Float>? = null
     override var distance: UserActionDistance? = null
@@ -226,3 +227,13 @@
         fractionRange(start, end, builder)
     }
 }
+
+internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), OverscrollBuilder {
+    override fun translate(
+        matcher: ElementMatcher,
+        x: OverscrollScope.() -> Float,
+        y: OverscrollScope.() -> Float
+    ) {
+        transformation(OverscrollTranslate(matcher, x, y))
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index 04d5914..849c9d7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -21,11 +21,11 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.OverscrollScope
 import com.android.compose.animation.scene.Scene
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
-/** Translate an element by a fixed amount of density-independent pixels. */
 internal class Translate(
     override val matcher: ElementMatcher,
     private val x: Dp = 0.dp,
@@ -47,3 +47,28 @@
         }
     }
 }
+
+internal class OverscrollTranslate(
+    override val matcher: ElementMatcher,
+    val x: OverscrollScope.() -> Float = { 0f },
+    val y: OverscrollScope.() -> Float = { 0f },
+) : PropertyTransformation<Offset> {
+    override fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneState: Element.SceneState,
+        transition: TransitionState.Transition,
+        value: Offset,
+    ): Offset {
+        // As this object is created by OverscrollBuilderImpl and we retrieve the current
+        // OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume
+        // that this method was invoked after performing this check.
+        val overscrollProperties = transition as TransitionState.HasOverscrollProperties
+
+        return Offset(
+            x = value.x + overscrollProperties.overscrollScope.x(),
+            y = value.y + overscrollProperties.overscrollScope.y(),
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 059a10e..26e01fe 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -539,24 +539,20 @@
         }
     }
 
-    @Test
-    fun elementTransitionDuringOverscroll() {
+    private fun setupOverscrollScenario(
+        layoutWidth: Dp,
+        layoutHeight: Dp,
+        sceneTransitions: SceneTransitionsBuilder.() -> Unit,
+        firstScroll: Float
+    ): MutableSceneTransitionLayoutStateImpl {
         // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
         // detected as a drag event.
         var touchSlop = 0f
-        val overscrollTranslateY = 10.dp
-        val layoutWidth = 200.dp
-        val layoutHeight = 400.dp
 
         val state =
             MutableSceneTransitionLayoutState(
                 initialScene = TestScenes.SceneA,
-                transitions =
-                    transitions {
-                        overscroll(TestScenes.SceneB, Orientation.Vertical) {
-                            translate(TestElements.Foo, y = overscrollTranslateY)
-                        }
-                    }
+                transitions = transitions(sceneTransitions),
             )
                 as MutableSceneTransitionLayoutStateImpl
 
@@ -585,9 +581,30 @@
         rule.onRoot().performTouchInput {
             val middleTop = Offset((layoutWidth / 2).toPx(), 0f)
             down(middleTop)
-            // Scroll 50%
-            moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
+            val firstScrollHeight = layoutHeight.toPx() * firstScroll
+            moveBy(Offset(0f, touchSlop + firstScrollHeight), delayMillis = 1_000)
         }
+        return state
+    }
+
+    @Test
+    fun elementTransitionDuringOverscroll() {
+        val layoutWidth = 200.dp
+        val layoutHeight = 400.dp
+        val overscrollTranslateY = 10.dp
+
+        val state =
+            setupOverscrollScenario(
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight,
+                sceneTransitions = {
+                    overscroll(TestScenes.SceneB, Orientation.Vertical) {
+                        // On overscroll 100% -> Foo should translate by overscrollTranslateY
+                        translate(TestElements.Foo, y = overscrollTranslateY)
+                    }
+                },
+                firstScroll = 0.5f, // Scroll 50%
+            )
 
         val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
         fooElement.assertTopPositionInRootIsEqualTo(0.dp)
@@ -691,4 +708,48 @@
         assertThat(state.currentOverscrollSpec).isNotNull()
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
     }
+
+    @Test
+    fun elementTransitionWithDistanceDuringOverscroll() {
+        val layoutWidth = 200.dp
+        val layoutHeight = 400.dp
+        val state =
+            setupOverscrollScenario(
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight,
+                sceneTransitions = {
+                    overscroll(TestScenes.SceneB, Orientation.Vertical) {
+                        // On overscroll 100% -> Foo should translate by layoutHeight
+                        translate(TestElements.Foo, y = { absoluteDistance })
+                    }
+                },
+                firstScroll = 1f, // 100% scroll
+            )
+
+        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
+        fooElement.assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onRoot().performTouchInput {
+            // Scroll another 50%
+            moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
+        }
+
+        val transition = state.currentTransition
+        assertThat(transition).isNotNull()
+
+        // Scroll 150% (100% scroll + 50% overscroll)
+        assertThat(transition!!.progress).isEqualTo(1.5f)
+        assertThat(state.currentOverscrollSpec).isNotNull()
+        fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
+
+        rule.onRoot().performTouchInput {
+            // Scroll another 100%
+            moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000)
+        }
+
+        // Scroll 250% (100% scroll + 150% overscroll)
+        assertThat(transition.progress).isEqualTo(2.5f)
+        assertThat(state.currentOverscrollSpec).isNotNull()
+        fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index c9c3ecc..825fe13 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -22,9 +22,9 @@
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.gestures.Orientation
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.transformation.OverscrollTranslate
 import com.android.compose.animation.scene.transformation.Transformation
 import com.android.compose.animation.scene.transformation.TransformationRange
-import com.android.compose.animation.scene.transformation.Translate
 import com.google.common.truth.Correspondence
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -228,12 +228,14 @@
     @Test
     fun overscrollSpec() {
         val transitions = transitions {
-            overscroll(TestScenes.SceneA, Orientation.Vertical) { translate(TestElements.Bar) }
+            overscroll(TestScenes.SceneA, Orientation.Vertical) {
+                translate(TestElements.Bar, x = { 1f }, y = { 2f })
+            }
         }
 
         val overscrollSpec = transitions.overscrollSpecs.single()
         val transformation = overscrollSpec.transformationSpec.transformations.single()
-        assertThat(transformation).isInstanceOf(Translate::class.java)
+        assertThat(transformation).isInstanceOf(OverscrollTranslate::class.java)
     }
 
     companion object {
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
index 153d2b8..73a66c6 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
@@ -38,6 +38,10 @@
         override val isUserInputOngoing: Boolean = isUserInputOngoing
         override val isUpOrLeft: Boolean = isUpOrLeft
         override val orientation: Orientation = orientation
+        override val overscrollScope: OverscrollScope =
+            object : OverscrollScope {
+                override val absoluteDistance = 0f
+            }
 
         override fun finish(): Job {
             error("finish() is not supported in test transitions")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
new file mode 100644
index 0000000..8f03717
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.qs
+
+import android.os.VibrationEffect
+import android.testing.TestableLooper.RunWithLooper
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.core.view.MotionEventBuilder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.AnimatorTestRule
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWithLooper(setAsMainLooper = true)
+class QSLongPressEffectTest : SysuiTestCase() {
+
+    @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
+    @Mock private lateinit var vibratorHelper: VibratorHelper
+    @Mock private lateinit var testView: View
+    @get:Rule val animatorTestRule = AnimatorTestRule(this)
+    private val kosmos = testKosmos()
+
+    private val effectDuration = 400
+    private val lowTickDuration = 12
+    private val spinDuration = 133
+
+    private lateinit var longPressEffect: QSLongPressEffect
+
+    @Before
+    fun setup() {
+        whenever(
+                vibratorHelper.getPrimitiveDurations(
+                    VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
+                    VibrationEffect.Composition.PRIMITIVE_SPIN,
+                )
+            )
+            .thenReturn(intArrayOf(lowTickDuration, spinDuration))
+
+        longPressEffect =
+            QSLongPressEffect(
+                vibratorHelper,
+                effectDuration,
+            )
+    }
+
+    @Test
+    fun onActionDown_whileIdle_startsWait() = testWithScope {
+        // GIVEN an action down event occurs
+        val downEvent = buildMotionEvent(MotionEvent.ACTION_DOWN)
+        longPressEffect.onTouch(testView, downEvent)
+
+        // THEN the effect moves to the TIMEOUT_WAIT state
+        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
+    }
+
+    @Test
+    fun onActionCancel_whileWaiting_goesIdle() = testWhileWaiting {
+        // GIVEN an action cancel occurs
+        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
+        longPressEffect.onTouch(testView, cancelEvent)
+
+        // THEN the effect goes back to idle and does not start
+        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+        assertEffectDidNotStart()
+    }
+
+    @Test
+    fun onActionUp_whileWaiting_performsClick() = testWhileWaiting {
+        // GIVEN an action is being collected
+        val action by collectLastValue(longPressEffect.actionType)
+
+        // GIVEN an action up occurs
+        val upEvent = buildMotionEvent(MotionEvent.ACTION_UP)
+        longPressEffect.onTouch(testView, upEvent)
+
+        // THEN the action to invoke is the click action and the effect does not start
+        assertThat(action).isEqualTo(QSLongPressEffect.ActionType.CLICK)
+        assertEffectDidNotStart()
+    }
+
+    @Test
+    fun onWaitComplete_whileWaiting_beginsEffect() = testWhileWaiting {
+        // GIVEN the pressed timeout is complete
+        advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L)
+
+        // THEN the effect starts
+        assertEffectStarted()
+    }
+
+    @Test
+    fun onActionUp_whileEffectHasBegun_reversesEffect() = testWhileRunning {
+        // GIVEN that the effect is at the middle of its completion (progress of 50%)
+        animatorTestRule.advanceTimeBy(effectDuration / 2L)
+
+        // WHEN an action up occurs
+        val upEvent = buildMotionEvent(MotionEvent.ACTION_UP)
+        longPressEffect.onTouch(testView, upEvent)
+
+        // THEN the effect gets reversed at 50% progress
+        assertEffectReverses(0.5f)
+    }
+
+    @Test
+    fun onActionCancel_whileEffectHasBegun_reversesEffect() = testWhileRunning {
+        // GIVEN that the effect is at the middle of its completion (progress of 50%)
+        animatorTestRule.advanceTimeBy(effectDuration / 2L)
+
+        // WHEN an action cancel occurs
+        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
+        longPressEffect.onTouch(testView, cancelEvent)
+
+        // THEN the effect gets reversed at 50% progress
+        assertEffectReverses(0.5f)
+    }
+
+    @Test
+    fun onAnimationComplete_effectEnds() = testWhileRunning {
+        // GIVEN that the animation completes
+        animatorTestRule.advanceTimeBy(effectDuration + 10L)
+
+        // THEN the long-press effect completes
+        assertEffectCompleted()
+    }
+
+    @Test
+    fun onActionDown_whileRunningBackwards_resets() = testWhileRunning {
+        // GIVEN that the effect is at the middle of its completion (progress of 50%)
+        animatorTestRule.advanceTimeBy(effectDuration / 2L)
+
+        // GIVEN an action cancel occurs and the effect gets reversed
+        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
+        longPressEffect.onTouch(testView, cancelEvent)
+
+        // GIVEN an action down occurs
+        val downEvent = buildMotionEvent(MotionEvent.ACTION_DOWN)
+        longPressEffect.onTouch(testView, downEvent)
+
+        // THEN the effect resets
+        assertEffectResets()
+    }
+
+    @Test
+    fun onAnimationComplete_whileRunningBackwards_goesToIdle() = testWhileRunning {
+        // GIVEN that the effect is at the middle of its completion (progress of 50%)
+        animatorTestRule.advanceTimeBy(effectDuration / 2L)
+
+        // GIVEN an action cancel occurs and the effect gets reversed
+        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
+        longPressEffect.onTouch(testView, cancelEvent)
+
+        // GIVEN that the animation completes after a sufficient amount of time
+        animatorTestRule.advanceTimeBy(effectDuration.toLong())
+
+        // THEN the state goes to [QSLongPressEffect.State.IDLE]
+        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+    }
+
+    private fun buildMotionEvent(action: Int): MotionEvent =
+        MotionEventBuilder.newBuilder().setAction(action).build()
+
+    private fun testWithScope(test: suspend TestScope.() -> Unit) =
+        with(kosmos) {
+            testScope.runTest {
+                // GIVEN an effect with a testing scope
+                longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
+
+                // THEN run the test
+                test()
+            }
+        }
+
+    private fun testWhileWaiting(test: suspend TestScope.() -> Unit) =
+        with(kosmos) {
+            testScope.runTest {
+                // GIVEN an effect with a testing scope
+                longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
+
+                // GIVEN the TIMEOUT_WAIT state is entered
+                val downEvent =
+                    MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build()
+                longPressEffect.onTouch(testView, downEvent)
+
+                // THEN run the test
+                test()
+            }
+        }
+
+    private fun testWhileRunning(test: suspend TestScope.() -> Unit) =
+        with(kosmos) {
+            testScope.runTest {
+                // GIVEN an effect with a testing scope
+                longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
+
+                // GIVEN the down event that enters the TIMEOUT_WAIT state
+                val downEvent =
+                    MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build()
+                longPressEffect.onTouch(testView, downEvent)
+
+                // GIVEN that the timeout completes and the effect starts
+                advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L)
+
+                // THEN run the test
+                test()
+            }
+        }
+
+    /**
+     * Asserts that the effect started by checking that:
+     * 1. The effect progress is 0f
+     * 2. Initial hint haptics are played
+     * 3. The internal state is [QSLongPressEffect.State.RUNNING_FORWARD]
+     */
+    private fun TestScope.assertEffectStarted() {
+        val effectProgress by collectLastValue(longPressEffect.effectProgress)
+        val longPressHint =
+            LongPressHapticBuilder.createLongPressHint(
+                lowTickDuration,
+                spinDuration,
+                effectDuration,
+            )
+
+        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
+        assertThat(effectProgress).isEqualTo(0f)
+        assertThat(longPressHint).isNotNull()
+        verify(vibratorHelper).vibrate(longPressHint!!)
+    }
+
+    /**
+     * Asserts that the effect did not start by checking that:
+     * 1. No effect progress is emitted
+     * 2. No haptics are played
+     * 3. The internal state is not [QSLongPressEffect.State.RUNNING_BACKWARDS] or
+     *    [QSLongPressEffect.State.RUNNING_FORWARD]
+     */
+    private fun TestScope.assertEffectDidNotStart() {
+        val effectProgress by collectLastValue(longPressEffect.effectProgress)
+
+        assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
+        assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
+        assertThat(effectProgress).isNull()
+        verify(vibratorHelper, never()).vibrate(any(/* type= */ VibrationEffect::class.java))
+    }
+
+    /**
+     * Asserts that the effect completes by checking that:
+     * 1. The progress is null
+     * 2. The final snap haptics are played
+     * 3. The internal state goes back to [QSLongPressEffect.State.IDLE]
+     * 4. The action to perform on the tile is the long-press action
+     */
+    private fun TestScope.assertEffectCompleted() {
+        val action by collectLastValue(longPressEffect.actionType)
+        val effectProgress by collectLastValue(longPressEffect.effectProgress)
+        val snapEffect = LongPressHapticBuilder.createSnapEffect()
+
+        assertThat(effectProgress).isNull()
+        assertThat(snapEffect).isNotNull()
+        verify(vibratorHelper).vibrate(snapEffect!!)
+        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+        assertThat(action).isEqualTo(QSLongPressEffect.ActionType.LONG_PRESS)
+    }
+
+    /**
+     * Assert that the effect gets reverted by checking that:
+     * 1. The internal state is [QSLongPressEffect.State.RUNNING_BACKWARDS]
+     * 2. The reverse haptics plays at the point where the animation was paused
+     */
+    private fun assertEffectReverses(pausedProgress: Float) {
+        val reverseHaptics =
+            LongPressHapticBuilder.createReversedEffect(
+                pausedProgress,
+                lowTickDuration,
+                effectDuration,
+            )
+
+        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
+        assertThat(reverseHaptics).isNotNull()
+        verify(vibratorHelper).vibrate(reverseHaptics!!)
+    }
+
+    /**
+     * Asserts that the effect resets by checking that:
+     * 1. The effect progress resets to 0
+     * 2. The internal state goes back to [QSLongPressEffect.State.TIMEOUT_WAIT]
+     */
+    private fun TestScope.assertEffectResets() {
+        val effectProgress by collectLastValue(longPressEffect.effectProgress)
+        assertThat(effectProgress).isEqualTo(0f)
+
+        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 19950a5..2fd2ef1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -19,9 +19,12 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.platform.test.annotations.EnableFlags
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -31,86 +34,129 @@
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.domain.startable.shadeStartable
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class LockscreenSceneViewModelTest : SysuiTestCase() {
 
+    companion object {
+        @Parameters(
+            name =
+                "canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," +
+                    " isSingleShade={3}, isCommunalAvailable={4}"
+        )
+        @JvmStatic
+        fun combinations() = buildList {
+            repeat(32) { combination ->
+                add(
+                    arrayOf(
+                        /* canSwipeToEnter= */ combination and 1 != 0,
+                        /* downWithTwoPointers= */ combination and 2 != 0,
+                        /* downFromEdge= */ combination and 4 != 0,
+                        /* isSingleShade= */ combination and 8 != 0,
+                        /* isCommunalAvailable= */ combination and 16 != 0,
+                    )
+                )
+            }
+        }
+
+        @JvmStatic
+        @BeforeClass
+        fun setUp() {
+            val combinationStrings =
+                combinations().map { array ->
+                    check(array.size == 5)
+                    "${array[4]},${array[3]},${array[2]},${array[1]},${array[0]}"
+                }
+            val uniqueCombinations = combinationStrings.toSet()
+            assertThat(combinationStrings).hasSize(uniqueCombinations.size)
+        }
+
+        private fun expectedDownDestination(
+            downFromEdge: Boolean,
+            isSingleShade: Boolean,
+        ): SceneKey {
+            return if (downFromEdge && isSingleShade) Scenes.QuickSettings else Scenes.Shade
+        }
+    }
+
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
 
+    @JvmField @Parameter(0) var canSwipeToEnter: Boolean = false
+    @JvmField @Parameter(1) var downWithTwoPointers: Boolean = false
+    @JvmField @Parameter(2) var downFromEdge: Boolean = false
+    @JvmField @Parameter(3) var isSingleShade: Boolean = true
+    @JvmField @Parameter(4) var isCommunalAvailable: Boolean = false
+
     private val underTest by lazy { createLockscreenSceneViewModel() }
 
     @Test
-    fun upTransitionSceneKey_canSwipeToUnlock_gone() =
+    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
+    fun destinationScenes() =
         testScope.runTest {
-            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.None
-            )
             kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
-            kosmos.fakeDeviceEntryRepository.setUnlocked(true)
-            sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
-
-            assertThat(upTransitionSceneKey).isEqualTo(Scenes.Gone)
-        }
-
-    @Test
-    fun upTransitionSceneKey_cannotSwipeToUnlock_bouncer() =
-        testScope.runTest {
-            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Pin
+                if (canSwipeToEnter) {
+                    AuthenticationMethodModel.None
+                } else {
+                    AuthenticationMethodModel.Pin
+                }
             )
-            kosmos.fakeDeviceEntryRepository.setUnlocked(false)
+            kosmos.fakeDeviceEntryRepository.setUnlocked(canSwipeToEnter)
             sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+            kosmos.shadeRepository.setShadeMode(
+                if (isSingleShade) {
+                    ShadeMode.Single
+                } else {
+                    ShadeMode.Split
+                }
+            )
+            kosmos.setCommunalAvailable(isCommunalAvailable)
 
-            assertThat(upTransitionSceneKey).isEqualTo(Scenes.Bouncer)
-        }
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
 
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @Test
-    fun leftTransitionSceneKey_communalIsAvailable_communal() =
-        testScope.runTest {
-            val leftDestinationSceneKey by collectLastValue(underTest.leftDestinationSceneKey)
-            assertThat(leftDestinationSceneKey).isNull()
+            assertThat(
+                    destinationScenes
+                        ?.get(
+                            Swipe(
+                                SwipeDirection.Down,
+                                fromSource = Edge.Top.takeIf { downFromEdge },
+                                pointerCount = if (downWithTwoPointers) 2 else 1,
+                            )
+                        )
+                        ?.toScene
+                )
+                .isEqualTo(
+                    expectedDownDestination(
+                        downFromEdge = downFromEdge,
+                        isSingleShade = isSingleShade,
+                    )
+                )
 
-            kosmos.setCommunalAvailable(true)
-            runCurrent()
-            assertThat(leftDestinationSceneKey).isEqualTo(Scenes.Communal)
-        }
+            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
+                .isEqualTo(if (canSwipeToEnter) Scenes.Gone else Scenes.Bouncer)
 
-    @Test
-    fun downFromTopEdgeDestinationSceneKey_whenNotSplitShade_quickSettings() =
-        testScope.runTest {
-            overrideResource(R.bool.config_use_split_notification_shade, false)
-            kosmos.shadeStartable.start()
-            val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey)
-            assertThat(sceneKey).isEqualTo(Scenes.QuickSettings)
-        }
-
-    @Test
-    fun downFromTopEdgeDestinationSceneKey_whenSplitShade_null() =
-        testScope.runTest {
-            overrideResource(R.bool.config_use_split_notification_shade, true)
-            kosmos.shadeStartable.start()
-            val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey)
-            assertThat(sceneKey).isNull()
+            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Left))?.toScene)
+                .isEqualTo(Scenes.Communal.takeIf { isCommunalAvailable })
         }
 
     private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 42c3354..af9abcd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -26,7 +26,6 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
 import com.android.internal.R
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.internal.util.emergencyAffordanceManager
@@ -317,8 +316,8 @@
     @Test
     fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() =
         testScope.runTest {
-            val upDestinationSceneKey by
-                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
             emulateUserDrivenTransition(
                 to = upDestinationSceneKey,
@@ -337,8 +336,8 @@
         testScope.runTest {
             setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
 
-            val upDestinationSceneKey by
-                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
             emulateUserDrivenTransition(
                 to = upDestinationSceneKey,
@@ -356,7 +355,7 @@
             emulateUserDrivenTransition(to = Scenes.Shade)
             assertCurrentScene(Scenes.Shade)
 
-            val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen)
             emulateUserDrivenTransition(
                 to = upDestinationSceneKey,
@@ -379,7 +378,7 @@
             emulateUserDrivenTransition(to = Scenes.Shade)
             assertCurrentScene(Scenes.Shade)
 
-            val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
             emulateUserDrivenTransition(
                 to = upDestinationSceneKey,
@@ -447,8 +446,8 @@
     fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() =
         testScope.runTest {
             unlockDevice()
-            val upDestinationSceneKey by
-                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
         }
 
@@ -469,8 +468,8 @@
     fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() =
         testScope.runTest {
             setAuthMethod(AuthenticationMethodModel.Password)
-            val upDestinationSceneKey by
-                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
             emulateUserDrivenTransition(
                 to = upDestinationSceneKey,
@@ -487,8 +486,8 @@
     fun bouncerActionButtonClick_opensEmergencyServicesDialer() =
         testScope.runTest {
             setAuthMethod(AuthenticationMethodModel.Password)
-            val upDestinationSceneKey by
-                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
             emulateUserDrivenTransition(to = upDestinationSceneKey)
 
@@ -507,8 +506,8 @@
         testScope.runTest {
             setAuthMethod(AuthenticationMethodModel.Password)
             startPhoneCall()
-            val upDestinationSceneKey by
-                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
+            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
             emulateUserDrivenTransition(to = upDestinationSceneKey)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 4fe45f6..8c9036a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.userRepository
 import com.google.common.truth.Truth
@@ -562,17 +563,17 @@
         }
 
     @Test
-    fun isSplitShade() =
+    fun shadeMode() =
         testScope.runTest {
-            val isSplitShade by collectLastValue(underTest.isSplitShade)
+            val shadeMode by collectLastValue(underTest.shadeMode)
 
-            shadeRepository.setSplitShade(true)
-            assertThat(isSplitShade).isTrue()
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Split)
 
-            shadeRepository.setSplitShade(false)
-            assertThat(isSplitShade).isFalse()
+            shadeRepository.setShadeMode(ShadeMode.Single)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Single)
 
-            shadeRepository.setSplitShade(true)
-            assertThat(isSplitShade).isTrue()
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Split)
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index cbb84da..31dacdd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -42,20 +43,20 @@
     private val underTest = kosmos.shadeStartable
 
     @Test
-    fun hydrateSplitShade() =
+    fun hydrateShadeMode() =
         testScope.runTest {
             overrideResource(R.bool.config_use_split_notification_shade, false)
-            val isSplitShade by collectLastValue(shadeInteractor.isSplitShade)
+            val shadeMode by collectLastValue(shadeInteractor.shadeMode)
 
             underTest.start()
-            assertThat(isSplitShade).isFalse()
+            assertThat(shadeMode).isEqualTo(ShadeMode.Single)
 
             overrideResource(R.bool.config_use_split_notification_shade, true)
             fakeConfigurationRepository.onAnyConfigurationChange()
-            assertThat(isSplitShade).isTrue()
+            assertThat(shadeMode).isEqualTo(ShadeMode.Split)
 
             overrideResource(R.bool.config_use_split_notification_shade, false)
             fakeConfigurationRepository.onAnyConfigurationChange()
-            assertThat(isSplitShade).isFalse()
+            assertThat(shadeMode).isEqualTo(ShadeMode.Single)
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 2e68d12..1c54961 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -37,10 +37,12 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.privacyChipInteractor
 import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.domain.startable.shadeStartable
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
@@ -72,6 +74,7 @@
     private val testScope = kosmos.testScope
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
+    private val shadeRepository by lazy { kosmos.shadeRepository }
 
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
@@ -274,4 +277,19 @@
             assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.toScene)
                 .isEqualTo(Scenes.QuickSettings)
         }
+
+    @Test
+    fun shadeMode() =
+        testScope.runTest {
+            val shadeMode by collectLastValue(underTest.shadeMode)
+
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+
+            shadeRepository.setShadeMode(ShadeMode.Single)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Single)
+
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
index a2f3ccb..e06efe8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
@@ -113,6 +113,28 @@
     }
 
     @Test
+    fun zenMuted_cantChange() {
+        with(kosmos) {
+            testScope.runTest {
+                notificationsSoundPolicyRepository.updateNotificationPolicy()
+                notificationsSoundPolicyRepository.updateZenMode(
+                    ZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
+                )
+
+                val canChangeVolume by
+                    collectLastValue(
+                        underTest.canChangeVolume(AudioStream(AudioManager.STREAM_NOTIFICATION))
+                    )
+
+                underTest.setMuted(AudioStream(AudioManager.STREAM_RING), true)
+                runCurrent()
+
+                assertThat(canChangeVolume).isFalse()
+            }
+        }
+    }
+
+    @Test
     fun streamIsMuted_getStream_volumeZero() {
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index f51e109..7341015 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -304,6 +304,15 @@
     <!-- An explanation text that the password needs to be entered since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] -->
     <string name="kg_prompt_reason_timeout_password">For additional security, use password instead</string>
 
+    <!-- An explanation text that the pin needs to be provided to enter the device for security reasons. [CHAR LIMIT=70] -->
+    <string name="kg_prompt_added_security_pin">PIN required for additional security</string>
+
+    <!-- An explanation text that the pattern needs to be provided to enter the device for security reasons. [CHAR LIMIT=70] -->
+    <string name="kg_prompt_added_security_pattern">Pattern required for additional security</string>
+
+    <!-- An explanation text that the password needs to be provided to enter the device for security reasons. [CHAR LIMIT=70] -->
+    <string name="kg_prompt_added_security_password">Password required for additional security</string>
+
     <!-- An explanation text that the credential needs to be entered because a device admin has
     locked the device. [CHAR LIMIT=80] -->
     <string name="kg_prompt_reason_device_admin">Device locked by admin</string>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 25596cc..b8f20f6 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1536,8 +1536,12 @@
 
     <!-- Media device casting volume slider label [CHAR_LIMIT=20] -->
     <string name="media_device_cast">Cast</string>
-    <!-- A message shown when the notification volume changing is disabled because of the muted ring stream [CHAR_LIMIT=40]-->
+    <!-- A message shown when the notification volume changing is disabled because of the muted ring stream [CHAR_LIMIT=50]-->
     <string name="stream_notification_unavailable">Unavailable because ring is muted</string>
+    <!-- A message shown when the alarm volume changing is disabled because of the don't disturb mode [CHAR_LIMIT=50]-->
+    <string name="stream_alarm_unavailable">Unavailable because Do Not Disturb is on</string>
+    <!-- A message shown when the media volume changing is disabled because of the don't disturb mode [CHAR_LIMIT=50]-->
+    <string name="stream_media_unavailable">Unavailable because Do Not Disturb is on</string>
 
     <!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on vibrate. [CHAR_LIMIT=NONE] -->
     <!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on silent (muted). [CHAR_LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 4e7809a..59516be 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -965,6 +965,10 @@
         <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
     </style>
 
+    <style name="Widget.SliceView.VolumePanel">
+        <item name="hideHeaderRow">true</item>
+    </style>
+
     <style name="Theme.VolumePanelActivity.Popup" parent="@style/Theme.SystemUI.Dialog">
         <item name="android:dialogCornerRadius">44dp</item>
         <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainerHigh
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 6e611fe..42ba05c 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -35,6 +35,9 @@
     srcs: [
         ":statslog-SystemUI-java-gen",
     ],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
 }
 
 android_library {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 84c8ea7..26e91b6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -122,7 +122,7 @@
             case PROMPT_REASON_USER_REQUEST:
                 return R.string.kg_prompt_after_user_lockdown_password;
             case PROMPT_REASON_PREPARE_FOR_UPDATE:
-                return R.string.kg_prompt_reason_timeout_password;
+                return R.string.kg_prompt_added_security_password;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_password;
             case PROMPT_REASON_TRUSTAGENT_EXPIRED:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index bf8900d..caa74780 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -323,7 +323,7 @@
                 resId = R.string.kg_prompt_after_user_lockdown_pattern;
                 break;
             case PROMPT_REASON_PREPARE_FOR_UPDATE:
-                resId = R.string.kg_prompt_reason_timeout_pattern;
+                resId = R.string.kg_prompt_added_security_pattern;
                 break;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 resId = R.string.kg_prompt_reason_timeout_pattern;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index bcab6f0..fbe9edf 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -134,7 +134,7 @@
             case PROMPT_REASON_USER_REQUEST:
                 return R.string.kg_prompt_after_user_lockdown_pin;
             case PROMPT_REASON_PREPARE_FOR_UPDATE:
-                return R.string.kg_prompt_reason_timeout_pin;
+                return R.string.kg_prompt_added_security_pin;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_pin;
             case PROMPT_REASON_TRUSTAGENT_EXPIRED:
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index c25e748..3092def 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -23,10 +23,12 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Flags
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.bouncer.data.repository.BouncerMessageRepository
 import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.bouncer.shared.model.BouncerMessageStrings
 import com.android.systemui.bouncer.shared.model.Message
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -35,46 +37,6 @@
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.TrustRepository
-import com.android.systemui.res.R.string.bouncer_face_not_recognized
-import com.android.systemui.res.R.string.keyguard_enter_password
-import com.android.systemui.res.R.string.keyguard_enter_pattern
-import com.android.systemui.res.R.string.keyguard_enter_pin
-import com.android.systemui.res.R.string.kg_bio_too_many_attempts_password
-import com.android.systemui.res.R.string.kg_bio_too_many_attempts_pattern
-import com.android.systemui.res.R.string.kg_bio_too_many_attempts_pin
-import com.android.systemui.res.R.string.kg_bio_try_again_or_password
-import com.android.systemui.res.R.string.kg_bio_try_again_or_pattern
-import com.android.systemui.res.R.string.kg_bio_try_again_or_pin
-import com.android.systemui.res.R.string.kg_face_locked_out
-import com.android.systemui.res.R.string.kg_fp_not_recognized
-import com.android.systemui.res.R.string.kg_primary_auth_locked_out_password
-import com.android.systemui.res.R.string.kg_primary_auth_locked_out_pattern
-import com.android.systemui.res.R.string.kg_primary_auth_locked_out_pin
-import com.android.systemui.res.R.string.kg_prompt_after_adaptive_auth_lock
-import com.android.systemui.res.R.string.kg_prompt_after_dpm_lock
-import com.android.systemui.res.R.string.kg_prompt_after_update_password
-import com.android.systemui.res.R.string.kg_prompt_after_update_pattern
-import com.android.systemui.res.R.string.kg_prompt_after_update_pin
-import com.android.systemui.res.R.string.kg_prompt_after_user_lockdown_password
-import com.android.systemui.res.R.string.kg_prompt_after_user_lockdown_pattern
-import com.android.systemui.res.R.string.kg_prompt_after_user_lockdown_pin
-import com.android.systemui.res.R.string.kg_prompt_auth_timeout
-import com.android.systemui.res.R.string.kg_prompt_password_auth_timeout
-import com.android.systemui.res.R.string.kg_prompt_pattern_auth_timeout
-import com.android.systemui.res.R.string.kg_prompt_pin_auth_timeout
-import com.android.systemui.res.R.string.kg_prompt_reason_restart_password
-import com.android.systemui.res.R.string.kg_prompt_reason_restart_pattern
-import com.android.systemui.res.R.string.kg_prompt_reason_restart_pin
-import com.android.systemui.res.R.string.kg_prompt_unattended_update
-import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
-import com.android.systemui.res.R.string.kg_trust_agent_disabled
-import com.android.systemui.res.R.string.kg_unlock_with_password_or_fp
-import com.android.systemui.res.R.string.kg_unlock_with_pattern_or_fp
-import com.android.systemui.res.R.string.kg_unlock_with_pin_or_fp
-import com.android.systemui.res.R.string.kg_wrong_input_try_fp_suggestion
-import com.android.systemui.res.R.string.kg_wrong_password_try_again
-import com.android.systemui.res.R.string.kg_wrong_pattern_try_again
-import com.android.systemui.res.R.string.kg_wrong_pin_try_again
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.kotlin.Quint
 import javax.inject.Inject
@@ -130,17 +92,22 @@
                 repository.setMessage(
                     when (biometricSourceType) {
                         BiometricSourceType.FINGERPRINT ->
-                            incorrectFingerprintInput(currentSecurityMode)
+                            BouncerMessageStrings.incorrectFingerprintInput(
+                                    currentSecurityMode.toAuthModel()
+                                )
+                                .toMessage()
                         BiometricSourceType.FACE ->
-                            incorrectFaceInput(
-                                currentSecurityMode,
-                                isFingerprintAuthCurrentlyAllowed.value
-                            )
+                            BouncerMessageStrings.incorrectFaceInput(
+                                    currentSecurityMode.toAuthModel(),
+                                    isFingerprintAuthCurrentlyAllowed.value
+                                )
+                                .toMessage()
                         else ->
-                            defaultMessage(
-                                currentSecurityMode,
-                                isFingerprintAuthCurrentlyAllowed.value
-                            )
+                            BouncerMessageStrings.defaultMessage(
+                                    currentSecurityMode.toAuthModel(),
+                                    isFingerprintAuthCurrentlyAllowed.value
+                                )
+                                .toMessage()
                     }
                 )
             }
@@ -189,45 +156,79 @@
                     trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot
                 ) {
                     if (wasRebootedForMainlineUpdate) {
-                        authRequiredForMainlineUpdate(currentSecurityMode)
+                        BouncerMessageStrings.authRequiredForMainlineUpdate(
+                                currentSecurityMode.toAuthModel()
+                            )
+                            .toMessage()
                     } else {
-                        authRequiredAfterReboot(currentSecurityMode)
+                        BouncerMessageStrings.authRequiredAfterReboot(
+                                currentSecurityMode.toAuthModel()
+                            )
+                            .toMessage()
                     }
                 } else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) {
-                    authRequiredAfterPrimaryAuthTimeout(currentSecurityMode)
+                    BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(
+                            currentSecurityMode.toAuthModel()
+                        )
+                        .toMessage()
                 } else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) {
-                    authRequiredAfterAdminLockdown(currentSecurityMode)
+                    BouncerMessageStrings.authRequiredAfterAdminLockdown(
+                            currentSecurityMode.toAuthModel()
+                        )
+                        .toMessage()
                 } else if (
                     trustOrBiometricsAvailable && flags.primaryAuthRequiredForUnattendedUpdate
                 ) {
-                    authRequiredForUnattendedUpdate(currentSecurityMode)
+                    BouncerMessageStrings.authRequiredForUnattendedUpdate(
+                            currentSecurityMode.toAuthModel()
+                        )
+                        .toMessage()
                 } else if (fpLockedOut) {
-                    class3AuthLockedOut(currentSecurityMode)
+                    BouncerMessageStrings.class3AuthLockedOut(currentSecurityMode.toAuthModel())
+                        .toMessage()
                 } else if (faceLockedOut) {
                     if (isFaceAuthClass3) {
-                        class3AuthLockedOut(currentSecurityMode)
+                        BouncerMessageStrings.class3AuthLockedOut(currentSecurityMode.toAuthModel())
+                            .toMessage()
                     } else {
-                        faceLockedOut(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value)
+                        BouncerMessageStrings.faceLockedOut(
+                                currentSecurityMode.toAuthModel(),
+                                isFingerprintAuthCurrentlyAllowed.value
+                            )
+                            .toMessage()
                     }
                 } else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) {
-                    authRequiredAfterAdaptiveAuthRequest(
-                        currentSecurityMode,
-                        isFingerprintAuthCurrentlyAllowed.value
-                    )
+                    BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
+                            currentSecurityMode.toAuthModel(),
+                            isFingerprintAuthCurrentlyAllowed.value
+                        )
+                        .toMessage()
                 } else if (
                     trustOrBiometricsAvailable &&
                         flags.strongerAuthRequiredAfterNonStrongBiometricsTimeout
                 ) {
-                    nonStrongAuthTimeout(
-                        currentSecurityMode,
-                        isFingerprintAuthCurrentlyAllowed.value
-                    )
+                    BouncerMessageStrings.nonStrongAuthTimeout(
+                            currentSecurityMode.toAuthModel(),
+                            isFingerprintAuthCurrentlyAllowed.value
+                        )
+                        .toMessage()
                 } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) {
-                    trustAgentDisabled(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value)
+                    BouncerMessageStrings.trustAgentDisabled(
+                            currentSecurityMode.toAuthModel(),
+                            isFingerprintAuthCurrentlyAllowed.value
+                        )
+                        .toMessage()
                 } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
-                    trustAgentDisabled(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value)
+                    BouncerMessageStrings.trustAgentDisabled(
+                            currentSecurityMode.toAuthModel(),
+                            isFingerprintAuthCurrentlyAllowed.value
+                        )
+                        .toMessage()
                 } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
-                    authRequiredAfterUserLockdown(currentSecurityMode)
+                    BouncerMessageStrings.authRequiredAfterUserLockdown(
+                            currentSecurityMode.toAuthModel()
+                        )
+                        .toMessage()
                 } else {
                     defaultMessage
                 }
@@ -244,7 +245,11 @@
 
                 override fun onTick(millisUntilFinished: Long) {
                     val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt()
-                    val message = primaryAuthLockedOut(currentSecurityMode)
+                    val message =
+                        BouncerMessageStrings.primaryAuthLockedOut(
+                                currentSecurityMode.toAuthModel()
+                            )
+                            .toMessage()
                     message.message?.animate = false
                     message.message?.formatterArgs =
                         mutableMapOf<String, Any>(Pair("count", secondsRemaining))
@@ -258,7 +263,11 @@
         if (!Flags.revampedBouncerMessages()) return
 
         repository.setMessage(
-            incorrectSecurityInput(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value)
+            BouncerMessageStrings.incorrectSecurityInput(
+                    currentSecurityMode.toAuthModel(),
+                    isFingerprintAuthCurrentlyAllowed.value
+                )
+                .toMessage()
         )
     }
 
@@ -285,7 +294,12 @@
     }
 
     private val defaultMessage: BouncerMessageModel
-        get() = defaultMessage(currentSecurityMode, isFingerprintAuthCurrentlyAllowed.value)
+        get() =
+            BouncerMessageStrings.defaultMessage(
+                    currentSecurityMode.toAuthModel(),
+                    isFingerprintAuthCurrentlyAllowed.value
+                )
+                .toMessage()
 
     fun onPrimaryBouncerUserInput() {
         if (!Flags.revampedBouncerMessages()) return
@@ -354,283 +368,35 @@
     return BouncerMessageModel(
         message =
             Message(
-                messageResId = defaultMessage(securityMode, fpAuthIsAllowed).message?.messageResId,
+                messageResId =
+                    BouncerMessageStrings.defaultMessage(
+                            securityMode.toAuthModel(),
+                            fpAuthIsAllowed
+                        )
+                        .toMessage()
+                        .message
+                        ?.messageResId,
                 animate = false
             ),
         secondaryMessage = Message(message = secondaryMessage, animate = false)
     )
 }
 
-private fun defaultMessage(
-    securityMode: SecurityMode,
-    fpAuthIsAllowed: Boolean
-): BouncerMessageModel {
-    return if (fpAuthIsAllowed) {
-        defaultMessageWithFingerprint(securityMode)
-    } else
-        when (securityMode) {
-            SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-            SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-            SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-            else -> Pair(0, 0)
-        }.toMessage()
-}
-
-private fun defaultMessageWithFingerprint(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, 0)
-        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, 0)
-        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, 0)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun incorrectSecurityInput(
-    securityMode: SecurityMode,
-    fpAuthIsAllowed: Boolean
-): BouncerMessageModel {
-    return if (fpAuthIsAllowed) {
-        incorrectSecurityInputWithFingerprint(securityMode)
-    } else
-        when (securityMode) {
-            SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, 0)
-            SecurityMode.Password -> Pair(kg_wrong_password_try_again, 0)
-            SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, 0)
-            else -> Pair(0, 0)
-        }.toMessage()
-}
-
-private fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, kg_wrong_input_try_fp_suggestion)
-        SecurityMode.Password -> Pair(kg_wrong_password_try_again, kg_wrong_input_try_fp_suggestion)
-        SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, kg_wrong_input_try_fp_suggestion)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun incorrectFingerprintInput(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pattern)
-        SecurityMode.Password -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_password)
-        SecurityMode.PIN -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pin)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun incorrectFaceInput(
-    securityMode: SecurityMode,
-    fpAuthIsAllowed: Boolean
-): BouncerMessageModel {
-    return if (fpAuthIsAllowed) incorrectFaceInputWithFingerprintAllowed(securityMode)
-    else
-        when (securityMode) {
-            SecurityMode.Pattern -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pattern)
-            SecurityMode.Password -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_password)
-            SecurityMode.PIN -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pin)
-            else -> Pair(0, 0)
-        }.toMessage()
-}
-
-private fun incorrectFaceInputWithFingerprintAllowed(
-    securityMode: SecurityMode
-): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, bouncer_face_not_recognized)
-        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, bouncer_face_not_recognized)
-        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, bouncer_face_not_recognized)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun biometricLockout(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern)
-        SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_bio_too_many_attempts_pin)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun authRequiredAfterReboot(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_reason_restart_pattern)
-        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_reason_restart_password)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_dpm_lock)
-        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_dpm_lock)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun authRequiredAfterAdaptiveAuthRequest(
-    securityMode: SecurityMode,
-    fpAuthIsAllowed: Boolean
-): BouncerMessageModel {
-    return if (fpAuthIsAllowed) authRequiredAfterAdaptiveAuthRequestFingerprintAllowed(securityMode)
-    else
-        return when (securityMode) {
-            SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_adaptive_auth_lock)
-            SecurityMode.Password ->
-                Pair(keyguard_enter_password, kg_prompt_after_adaptive_auth_lock)
-            SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_adaptive_auth_lock)
-            else -> Pair(0, 0)
-        }.toMessage()
-}
-
-private fun authRequiredAfterAdaptiveAuthRequestFingerprintAllowed(
-    securityMode: SecurityMode
-): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern ->
-            Pair(kg_unlock_with_pattern_or_fp, kg_prompt_after_adaptive_auth_lock)
-        SecurityMode.Password ->
-            Pair(kg_unlock_with_password_or_fp, kg_prompt_after_adaptive_auth_lock)
-        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_prompt_after_adaptive_auth_lock)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun authRequiredAfterUserLockdown(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_user_lockdown_pattern)
-        SecurityMode.Password ->
-            Pair(keyguard_enter_password, kg_prompt_after_user_lockdown_password)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_unattended_update)
-        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_unattended_update)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_unattended_update)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun authRequiredForMainlineUpdate(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_update_pattern)
-        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_update_password)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_update_pin)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout)
-        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_password_auth_timeout)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun nonStrongAuthTimeout(
-    securityMode: SecurityMode,
-    fpAuthIsAllowed: Boolean
-): BouncerMessageModel {
-    return if (fpAuthIsAllowed) {
-        nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
-    } else
-        when (securityMode) {
-            SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_auth_timeout)
-            SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_auth_timeout)
-            SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_auth_timeout)
-            else -> Pair(0, 0)
-        }.toMessage()
-}
-
-fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_prompt_auth_timeout)
-        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_prompt_auth_timeout)
-        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_prompt_auth_timeout)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun faceLockedOut(
-    securityMode: SecurityMode,
-    fpAuthIsAllowed: Boolean
-): BouncerMessageModel {
-    return if (fpAuthIsAllowed) faceLockedOutButFingerprintAvailable(securityMode)
-    else
-        when (securityMode) {
-            SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out)
-            SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out)
-            SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_face_locked_out)
-            else -> Pair(0, 0)
-        }.toMessage()
-}
-
-private fun faceLockedOutButFingerprintAvailable(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_face_locked_out)
-        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_face_locked_out)
-        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_face_locked_out)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun class3AuthLockedOut(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern)
-        SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_bio_too_many_attempts_pin)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun trustAgentDisabled(
-    securityMode: SecurityMode,
-    fpAuthIsAllowed: Boolean
-): BouncerMessageModel {
-    return if (fpAuthIsAllowed) trustAgentDisabledWithFingerprintAllowed(securityMode)
-    else
-        when (securityMode) {
-            SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_trust_agent_disabled)
-            SecurityMode.Password -> Pair(keyguard_enter_password, kg_trust_agent_disabled)
-            SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_trust_agent_disabled)
-            else -> Pair(0, 0)
-        }.toMessage()
-}
-
-private fun trustAgentDisabledWithFingerprintAllowed(
-    securityMode: SecurityMode
-): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_trust_agent_disabled)
-        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_trust_agent_disabled)
-        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_trust_agent_disabled)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
-private fun primaryAuthLockedOut(securityMode: SecurityMode): BouncerMessageModel {
-    return when (securityMode) {
-        SecurityMode.Pattern ->
-            Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pattern)
-        SecurityMode.Password ->
-            Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_password)
-        SecurityMode.PIN ->
-            Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pin)
-        else -> Pair(0, 0)
-    }.toMessage()
-}
-
 private fun Pair<Int, Int>.toMessage(): BouncerMessageModel {
     return BouncerMessageModel(
         message = Message(messageResId = this.first, animate = false),
         secondaryMessage = Message(messageResId = this.second, animate = false)
     )
 }
+
+private fun SecurityMode.toAuthModel(): AuthenticationMethodModel {
+    return when (this) {
+        SecurityMode.Invalid -> AuthenticationMethodModel.None
+        SecurityMode.None -> AuthenticationMethodModel.None
+        SecurityMode.Pattern -> AuthenticationMethodModel.Pattern
+        SecurityMode.Password -> AuthenticationMethodModel.Password
+        SecurityMode.PIN -> AuthenticationMethodModel.Pin
+        SecurityMode.SimPin -> AuthenticationMethodModel.Sim
+        SecurityMode.SimPuk -> AuthenticationMethodModel.Sim
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageStrings.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageStrings.kt
new file mode 100644
index 0000000..cb12ce5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageStrings.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.shared.model
+
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
+import com.android.systemui.res.R
+
+typealias BouncerMessagePair = Pair<Int, Int>
+
+val BouncerMessagePair.primaryMessage: Int
+    get() = this.first
+
+val BouncerMessagePair.secondaryMessage: Int
+    get() = this.second
+
+object BouncerMessageStrings {
+    private val EmptyMessage = Pair(0, 0)
+
+    fun defaultMessage(
+        securityMode: AuthenticationMethodModel,
+        fpAuthIsAllowed: Boolean
+    ): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), 0)
+            Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), 0)
+            Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), 0)
+            else -> EmptyMessage
+        }
+    }
+
+    fun incorrectSecurityInput(
+        securityMode: AuthenticationMethodModel,
+        fpAuthIsAllowed: Boolean
+    ): BouncerMessagePair {
+        val secondaryMessage = incorrectSecurityInputSecondaryMessage(fpAuthIsAllowed)
+        return when (securityMode) {
+            Pattern -> Pair(R.string.kg_wrong_pattern_try_again, secondaryMessage)
+            Password -> Pair(R.string.kg_wrong_password_try_again, secondaryMessage)
+            Pin -> Pair(R.string.kg_wrong_pin_try_again, secondaryMessage)
+            else -> EmptyMessage
+        }
+    }
+
+    private fun incorrectSecurityInputSecondaryMessage(fpAuthIsAllowed: Boolean): Int {
+        return if (fpAuthIsAllowed) R.string.kg_wrong_input_try_fp_suggestion else 0
+    }
+
+    fun incorrectFingerprintInput(securityMode: AuthenticationMethodModel): BouncerMessagePair {
+        val primaryMessage = R.string.kg_fp_not_recognized
+        return when (securityMode) {
+            Pattern -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pattern)
+            Password -> Pair(primaryMessage, R.string.kg_bio_try_again_or_password)
+            Pin -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pin)
+            else -> EmptyMessage
+        }
+    }
+
+    fun incorrectFaceInput(
+        securityMode: AuthenticationMethodModel,
+        fpAuthIsAllowed: Boolean
+    ): BouncerMessagePair {
+        return if (fpAuthIsAllowed) incorrectFaceInputWithFingerprintAllowed(securityMode)
+        else {
+            val primaryMessage = R.string.bouncer_face_not_recognized
+            when (securityMode) {
+                Pattern -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pattern)
+                Password -> Pair(primaryMessage, R.string.kg_bio_try_again_or_password)
+                Pin -> Pair(primaryMessage, R.string.kg_bio_try_again_or_pin)
+                else -> EmptyMessage
+            }
+        }
+    }
+
+    private fun incorrectFaceInputWithFingerprintAllowed(
+        securityMode: AuthenticationMethodModel
+    ): BouncerMessagePair {
+        val secondaryMsg = R.string.bouncer_face_not_recognized
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(true), secondaryMsg)
+            Password -> Pair(passwordDefaultMessage(true), secondaryMsg)
+            Pin -> Pair(pinDefaultMessage(true), secondaryMsg)
+            else -> EmptyMessage
+        }
+    }
+
+    fun authRequiredAfterReboot(securityMode: AuthenticationMethodModel): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_reason_restart_pattern)
+            Password ->
+                Pair(passwordDefaultMessage(false), R.string.kg_prompt_reason_restart_password)
+            Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_reason_restart_pin)
+            else -> EmptyMessage
+        }
+    }
+
+    fun authRequiredAfterAdminLockdown(
+        securityMode: AuthenticationMethodModel
+    ): BouncerMessagePair {
+        val secondaryMsg = R.string.kg_prompt_after_dpm_lock
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(false), secondaryMsg)
+            Password -> Pair(passwordDefaultMessage(false), secondaryMsg)
+            Pin -> Pair(pinDefaultMessage(false), secondaryMsg)
+            else -> EmptyMessage
+        }
+    }
+
+    fun authRequiredAfterAdaptiveAuthRequest(
+        securityMode: AuthenticationMethodModel,
+        fpAuthIsAllowed: Boolean
+    ): BouncerMessagePair {
+        val secondaryMsg = R.string.kg_prompt_after_adaptive_auth_lock
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            else -> EmptyMessage
+        }
+    }
+
+    fun authRequiredAfterUserLockdown(securityMode: AuthenticationMethodModel): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern ->
+                Pair(patternDefaultMessage(false), R.string.kg_prompt_after_user_lockdown_pattern)
+            Password ->
+                Pair(passwordDefaultMessage(false), R.string.kg_prompt_after_user_lockdown_password)
+            Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_after_user_lockdown_pin)
+            else -> EmptyMessage
+        }
+    }
+
+    fun authRequiredForUnattendedUpdate(
+        securityMode: AuthenticationMethodModel
+    ): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_added_security_pattern)
+            Password ->
+                Pair(passwordDefaultMessage(false), R.string.kg_prompt_added_security_password)
+            Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_added_security_pin)
+            else -> EmptyMessage
+        }
+    }
+
+    fun authRequiredForMainlineUpdate(securityMode: AuthenticationMethodModel): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_after_update_pattern)
+            Password ->
+                Pair(passwordDefaultMessage(false), R.string.kg_prompt_after_update_password)
+            Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_after_update_pin)
+            else -> EmptyMessage
+        }
+    }
+
+    fun authRequiredAfterPrimaryAuthTimeout(
+        securityMode: AuthenticationMethodModel
+    ): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(false), R.string.kg_prompt_pattern_auth_timeout)
+            Password ->
+                Pair(passwordDefaultMessage(false), R.string.kg_prompt_password_auth_timeout)
+            Pin -> Pair(pinDefaultMessage(false), R.string.kg_prompt_pin_auth_timeout)
+            else -> EmptyMessage
+        }
+    }
+
+    fun nonStrongAuthTimeout(
+        securityMode: AuthenticationMethodModel,
+        fpAuthIsAllowed: Boolean
+    ): BouncerMessagePair {
+        val secondaryMsg = R.string.kg_prompt_auth_timeout
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            else -> EmptyMessage
+        }
+    }
+
+    fun faceLockedOut(
+        securityMode: AuthenticationMethodModel,
+        fpAuthIsAllowed: Boolean
+    ): BouncerMessagePair {
+        val secondaryMsg = R.string.kg_face_locked_out
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            else -> EmptyMessage
+        }
+    }
+
+    fun class3AuthLockedOut(securityMode: AuthenticationMethodModel): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(false), R.string.kg_bio_too_many_attempts_pattern)
+            Password ->
+                Pair(passwordDefaultMessage(false), R.string.kg_bio_too_many_attempts_password)
+            Pin -> Pair(pinDefaultMessage(false), R.string.kg_bio_too_many_attempts_pin)
+            else -> EmptyMessage
+        }
+    }
+
+    fun trustAgentDisabled(
+        securityMode: AuthenticationMethodModel,
+        fpAuthIsAllowed: Boolean
+    ): BouncerMessagePair {
+        val secondaryMsg = R.string.kg_trust_agent_disabled
+        return when (securityMode) {
+            Pattern -> Pair(patternDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Password -> Pair(passwordDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            Pin -> Pair(pinDefaultMessage(fpAuthIsAllowed), secondaryMsg)
+            else -> EmptyMessage
+        }
+    }
+
+    fun primaryAuthLockedOut(securityMode: AuthenticationMethodModel): BouncerMessagePair {
+        return when (securityMode) {
+            Pattern ->
+                Pair(
+                    R.string.kg_too_many_failed_attempts_countdown,
+                    R.string.kg_primary_auth_locked_out_pattern
+                )
+            Password ->
+                Pair(
+                    R.string.kg_too_many_failed_attempts_countdown,
+                    R.string.kg_primary_auth_locked_out_password
+                )
+            Pin ->
+                Pair(
+                    R.string.kg_too_many_failed_attempts_countdown,
+                    R.string.kg_primary_auth_locked_out_pin
+                )
+            else -> EmptyMessage
+        }
+    }
+
+    private fun patternDefaultMessage(fingerprintAllowed: Boolean): Int {
+        return if (fingerprintAllowed) R.string.kg_unlock_with_pattern_or_fp
+        else R.string.keyguard_enter_pattern
+    }
+
+    private fun pinDefaultMessage(fingerprintAllowed: Boolean): Int {
+        return if (fingerprintAllowed) R.string.kg_unlock_with_pin_or_fp
+        else R.string.keyguard_enter_pin
+    }
+
+    private fun passwordDefaultMessage(fingerprintAllowed: Boolean): Int {
+        return if (fingerprintAllowed) R.string.kg_unlock_with_password_or_fp
+        else R.string.keyguard_enter_password
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 6bb84649..a199fea 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -76,23 +76,6 @@
     val NOTIFICATION_MEMORY_LOGGING_ENABLED =
             releasedFlag("notification_memory_logging_enabled")
 
-    // TODO(b/260335638): Tracking Bug
-    @JvmField
-    val NOTIFICATION_INLINE_REPLY_ANIMATION = releasedFlag("notification_inline_reply_animation")
-
-    // TODO(b/288326013): Tracking Bug
-    @JvmField
-    val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
-        unreleasedFlag("notification_async_hybrid_view_inflation", teamfood = false)
-
-    @JvmField
-    val ANIMATED_NOTIFICATION_SHADE_INSETS =
-        releasedFlag("animated_notification_shade_insets")
-
-    // TODO(b/268005230): Tracking Bug
-    @JvmField
-    val SENSITIVE_REVEAL_ANIM = releasedFlag("sensitive_reveal_anim")
-
     // TODO(b/280783617): Tracking Bug
     @Keep
     @JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/LongPressHapticBuilder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/LongPressHapticBuilder.kt
new file mode 100644
index 0000000..0143b85
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/LongPressHapticBuilder.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.qs
+
+import android.os.VibrationEffect
+import android.util.Log
+import kotlin.math.max
+
+object LongPressHapticBuilder {
+
+    const val INVALID_DURATION = 0 /* in ms */
+
+    private const val TAG = "LongPressHapticBuilder"
+    private const val SPIN_SCALE = 0.2f
+    private const val CLICK_SCALE = 0.5f
+    private const val LOW_TICK_SCALE = 0.08f
+    private const val WARMUP_TIME = 75 /* in ms */
+    private const val DAMPING_TIME = 24 /* in ms */
+
+    /** Create the signal that indicates that a long-press action is available. */
+    fun createLongPressHint(
+        lowTickDuration: Int,
+        spinDuration: Int,
+        effectDuration: Int
+    ): VibrationEffect? {
+        if (lowTickDuration == 0 || spinDuration == 0) {
+            Log.d(
+                TAG,
+                "The LOW_TICK and/or SPIN primitives are not supported. No signal created.",
+            )
+            return null
+        }
+        if (effectDuration < WARMUP_TIME + spinDuration + DAMPING_TIME) {
+            Log.d(
+                TAG,
+                "Cannot fit long-press hint signal in the effect duration. No signal created",
+            )
+            return null
+        }
+
+        val nLowTicks = WARMUP_TIME / lowTickDuration
+        val rampDownLowTicks = DAMPING_TIME / lowTickDuration
+        val composition = VibrationEffect.startComposition()
+
+        // Warmup low ticks
+        repeat(nLowTicks) {
+            composition.addPrimitive(
+                VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
+                LOW_TICK_SCALE,
+                0,
+            )
+        }
+
+        // Spin effect
+        composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, SPIN_SCALE, 0)
+
+        // Damping low ticks
+        repeat(rampDownLowTicks) { i ->
+            composition.addPrimitive(
+                VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
+                LOW_TICK_SCALE / (i + 1),
+                0,
+            )
+        }
+
+        return composition.compose()
+    }
+
+    /** Create a "snapping" effect that triggers at the end of a long-press gesture */
+    fun createSnapEffect(): VibrationEffect? =
+        VibrationEffect.startComposition()
+            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, CLICK_SCALE, 0)
+            .compose()
+
+    /** Creates a signal that indicates the reversal of the long-press animation. */
+    fun createReversedEffect(
+        pausedProgress: Float,
+        lowTickDuration: Int,
+        effectDuration: Int,
+    ): VibrationEffect? {
+        val duration = pausedProgress * effectDuration
+        if (duration == 0f) return null
+
+        if (lowTickDuration == 0) {
+            Log.d(TAG, "Cannot play reverse haptics because LOW_TICK is not supported")
+            return null
+        }
+
+        val nLowTicks = (duration / lowTickDuration).toInt()
+        if (nLowTicks == 0) return null
+
+        val composition = VibrationEffect.startComposition()
+        var scale: Float
+        val step = LOW_TICK_SCALE / nLowTicks
+        repeat(nLowTicks) { i ->
+            scale = max(LOW_TICK_SCALE - step * i, 0f)
+            composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, scale, 0)
+        }
+        return composition.compose()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
new file mode 100644
index 0000000..ec72a14
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.qs
+
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.os.VibrationEffect
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.animation.AccelerateDecelerateInterpolator
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.doOnCancel
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import com.android.systemui.statusbar.VibratorHelper
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * A class that handles the long press visuo-haptic effect for a QS tile.
+ *
+ * The class is also a [View.OnTouchListener] to handle the touch events, clicks and long-press
+ * gestures of the tile. The class also provides a [State] that can be used to determine the current
+ * state of the long press effect.
+ *
+ * @property[vibratorHelper] The [VibratorHelper] to deliver haptic effects.
+ * @property[effectDuration] The duration of the effect in ms.
+ */
+class QSLongPressEffect(
+    private val vibratorHelper: VibratorHelper?,
+    private val effectDuration: Int,
+) : View.OnTouchListener {
+
+    /** Current state */
+    var state = State.IDLE
+        @VisibleForTesting set
+
+    /** Flows for view control and action */
+    private val _effectProgress = MutableStateFlow<Float?>(null)
+    val effectProgress = _effectProgress.asStateFlow()
+
+    private val _actionType = MutableStateFlow<ActionType?>(null)
+    val actionType = _actionType.asStateFlow()
+
+    /** Haptic effects */
+    private val durations =
+        vibratorHelper?.getPrimitiveDurations(
+            VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
+            VibrationEffect.Composition.PRIMITIVE_SPIN
+        )
+
+    private val longPressHint =
+        LongPressHapticBuilder.createLongPressHint(
+            durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
+            durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
+            effectDuration
+        )
+
+    private val snapEffect = LongPressHapticBuilder.createSnapEffect()
+
+    /* A coroutine scope and a timer job that waits for the pressedTimeout */
+    var scope: CoroutineScope? = null
+    private var waitJob: Job? = null
+
+    private val effectAnimator =
+        ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = effectDuration.toLong()
+            interpolator = AccelerateDecelerateInterpolator()
+
+            doOnStart { handleAnimationStart() }
+            addUpdateListener { _effectProgress.value = animatedValue as Float }
+            doOnEnd { handleAnimationComplete() }
+            doOnCancel { handleAnimationCancel() }
+        }
+
+    private fun reverse() {
+        val pausedProgress = effectAnimator.animatedFraction
+        val effect =
+            LongPressHapticBuilder.createReversedEffect(
+                pausedProgress,
+                durations?.get(0) ?: 0,
+                effectDuration,
+            )
+        vibratorHelper?.cancel()
+        vibrate(effect)
+        effectAnimator.reverse()
+    }
+
+    private fun vibrate(effect: VibrationEffect?) {
+        if (vibratorHelper != null && effect != null) {
+            vibratorHelper.vibrate(effect)
+        }
+    }
+
+    /**
+     * Handle relevant touch events for the operation of a Tile.
+     *
+     * A click action is performed following the relevant logic that originates from the
+     * [MotionEvent.ACTION_UP] event depending on the current state.
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onTouch(view: View?, event: MotionEvent?): Boolean {
+        when (event?.actionMasked) {
+            MotionEvent.ACTION_DOWN -> handleActionDown()
+            MotionEvent.ACTION_UP -> handleActionUp()
+            MotionEvent.ACTION_CANCEL -> handleActionCancel()
+        }
+        return true
+    }
+
+    private fun handleActionDown() {
+        when (state) {
+            State.IDLE -> {
+                startPressedTimeoutWait()
+                state = State.TIMEOUT_WAIT
+            }
+            State.RUNNING_BACKWARDS -> effectAnimator.cancel()
+            else -> {}
+        }
+    }
+
+    private fun startPressedTimeoutWait() {
+        waitJob =
+            scope?.launch {
+                try {
+                    delay(PRESSED_TIMEOUT)
+                    handleTimeoutComplete()
+                } catch (_: CancellationException) {
+                    state = State.IDLE
+                }
+            }
+    }
+
+    private fun handleActionUp() {
+        when (state) {
+            State.TIMEOUT_WAIT -> {
+                waitJob?.cancel()
+                _actionType.value = ActionType.CLICK
+                state = State.IDLE
+            }
+            State.RUNNING_FORWARD -> {
+                reverse()
+                state = State.RUNNING_BACKWARDS
+            }
+            else -> {}
+        }
+    }
+
+    private fun handleActionCancel() {
+        when (state) {
+            State.TIMEOUT_WAIT -> {
+                waitJob?.cancel()
+                state = State.IDLE
+            }
+            State.RUNNING_FORWARD -> {
+                reverse()
+                state = State.RUNNING_BACKWARDS
+            }
+            else -> {}
+        }
+    }
+
+    private fun handleAnimationStart() {
+        vibrate(longPressHint)
+        state = State.RUNNING_FORWARD
+    }
+
+    /** This function is called both when an animator completes or gets cancelled */
+    private fun handleAnimationComplete() {
+        if (state == State.RUNNING_FORWARD) {
+            vibrate(snapEffect)
+            _actionType.value = ActionType.LONG_PRESS
+            _effectProgress.value = null
+        }
+        if (state != State.TIMEOUT_WAIT) {
+            // This will happen if the animator did not finish by being cancelled
+            state = State.IDLE
+        }
+    }
+
+    private fun handleAnimationCancel() {
+        _effectProgress.value = 0f
+        startPressedTimeoutWait()
+        state = State.TIMEOUT_WAIT
+    }
+
+    private fun handleTimeoutComplete() {
+        if (state == State.TIMEOUT_WAIT && !effectAnimator.isRunning) {
+            effectAnimator.start()
+        }
+    }
+
+    fun clearActionType() {
+        _actionType.value = null
+    }
+
+    enum class State {
+        IDLE, /* The effect is idle waiting for touch input */
+        TIMEOUT_WAIT, /* The effect is waiting for a [PRESSED_TIMEOUT] period */
+        RUNNING_FORWARD, /* The effect is running normally */
+        RUNNING_BACKWARDS, /* The effect was interrupted and is now running backwards */
+    }
+
+    /* A type of action to perform on the view depending on the effect's state and logic */
+    enum class ActionType {
+        CLICK,
+        LONG_PRESS,
+    }
+
+    companion object {
+        /**
+         * A timeout to let the tile resolve if it is being swiped/scrolled. Since QS tiles are
+         * inside a scrollable container, they will be considered pressed only after a tap timeout.
+         */
+        val PRESSED_TIMEOUT = ViewConfiguration.getTapTimeout().toLong() + 20L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
new file mode 100644
index 0000000..e298154
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.qs
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.qs.tileimpl.QSTileViewImpl
+import kotlinx.coroutines.launch
+
+object QSLongPressEffectViewBinder {
+
+    fun bind(
+        tile: QSTileViewImpl,
+        effect: QSLongPressEffect?,
+    ) {
+        if (effect == null) return
+
+        tile.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                effect.scope = this
+
+                launch {
+                    effect.effectProgress.collect { progress ->
+                        progress?.let {
+                            if (it == 0f) {
+                                tile.bringToFront()
+                            }
+                            tile.updateLongPressEffectProperties(it)
+                        }
+                    }
+                }
+
+                launch {
+                    effect.actionType.collect { action ->
+                        action?.let {
+                            when (it) {
+                                QSLongPressEffect.ActionType.CLICK -> tile.performClick()
+                                QSLongPressEffect.ActionType.LONG_PRESS -> tile.performLongClick()
+                            }
+                            effect.clearActionType()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 106fdf1..5565ee2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
@@ -107,6 +108,7 @@
     private val lockscreenContentViewModel: LockscreenContentViewModel,
     private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>,
     private val keyguardBlueprintViewBinder: KeyguardBlueprintViewBinder,
+    private val clockInteractor: KeyguardClockInteractor,
 ) : CoreStartable {
 
     private var rootViewHandle: DisposableHandle? = null
@@ -220,7 +222,11 @@
             blueprints.mapNotNull { it as? ComposableLockscreenSceneBlueprint }.toSet()
         return ComposeView(context).apply {
             setContent {
-                LockscreenContent(viewModel = viewModel, blueprints = sceneBlueprints)
+                LockscreenContent(
+                        viewModel = viewModel,
+                        blueprints = sceneBlueprints,
+                        clockInteractor = clockInteractor
+                    )
                     .Content(modifier = Modifier.fillMaxSize())
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
index e5b5964..abf2372 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -79,6 +79,8 @@
 
     val notificationAlpha: Flow<Float> = keyguardAlpha
 
+    val shortcutsAlpha: Flow<Float> = keyguardAlpha
+
     val notificationTranslationX: Flow<Float> =
         keyguardTranslationX.map { it.value }.filterNotNull()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index f981fd5..58c45c7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -36,6 +36,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 @SysUISingleton
@@ -67,7 +68,16 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = LARGE
+                initialValue = LARGE,
+            )
+
+    val isLargeClockVisible =
+        clockSize
+            .map { it == LARGE }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
             )
 
     val currentClock = keyguardClockInteractor.currentClock
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 4db942cc..c4383fc 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
@@ -52,6 +52,7 @@
     occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
     offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
+    glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
     lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
     lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
     lockscreenToDreamingHostedTransitionViewModel: LockscreenToDreamingHostedTransitionViewModel,
@@ -59,6 +60,7 @@
     lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
     lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
     lockscreenToPrimaryBouncerTransitionViewModel: LockscreenToPrimaryBouncerTransitionViewModel,
+    lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
     transitionInteractor: KeyguardTransitionInteractor,
 ) {
 
@@ -110,6 +112,7 @@
             occludedToLockscreenTransitionViewModel.shortcutsAlpha,
             offToLockscreenTransitionViewModel.shortcutsAlpha,
             primaryBouncerToLockscreenTransitionViewModel.shortcutsAlpha,
+            glanceableHubToLockscreenTransitionViewModel.shortcutsAlpha,
         )
 
     /** alpha while fading the quick affordances in */
@@ -122,6 +125,7 @@
             lockscreenToGoneTransitionViewModel.shortcutsAlpha,
             lockscreenToOccludedTransitionViewModel.shortcutsAlpha,
             lockscreenToPrimaryBouncerTransitionViewModel.shortcutsAlpha,
+            lockscreenToGlanceableHubTransitionViewModel.shortcutsAlpha,
             shadeExpansionAlpha,
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index d89523d..993e81b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -14,21 +14,29 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
 
 /** Models UI state and handles user input for the lockscreen scene. */
@@ -43,37 +51,69 @@
     val longPress: KeyguardLongPressViewModel,
     val notifications: NotificationsPlaceholderViewModel,
 ) {
-    /** The key of the scene we should switch to when swiping up. */
-    val upDestinationSceneKey: StateFlow<SceneKey> =
-        deviceEntryInteractor.isUnlocked
-            .map { isUnlocked -> upDestinationSceneKey(isUnlocked) }
+    val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+        combine(
+                deviceEntryInteractor.isUnlocked,
+                communalInteractor.isCommunalAvailable,
+                shadeInteractor.shadeMode,
+            ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
+                destinationScenes(
+                    isDeviceUnlocked = isDeviceUnlocked,
+                    isCommunalAvailable = isCommunalAvailable,
+                    shadeMode = shadeMode,
+                )
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = upDestinationSceneKey(deviceEntryInteractor.isUnlocked.value),
+                initialValue =
+                    destinationScenes(
+                        isDeviceUnlocked = deviceEntryInteractor.isUnlocked.value,
+                        isCommunalAvailable = false,
+                        shadeMode = shadeInteractor.shadeMode.value,
+                    ),
             )
 
-    private fun upDestinationSceneKey(isUnlocked: Boolean): SceneKey {
-        return if (isUnlocked) Scenes.Gone else Scenes.Bouncer
+    private fun destinationScenes(
+        isDeviceUnlocked: Boolean,
+        isCommunalAvailable: Boolean,
+        shadeMode: ShadeMode,
+    ): Map<UserAction, UserActionResult> {
+        val quickSettingsIfSingleShade =
+            if (shadeMode is ShadeMode.Single) {
+                Scenes.QuickSettings
+            } else {
+                Scenes.Shade
+            }
+
+        return mapOf(
+                Swipe.Left to UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable },
+                Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer,
+
+                // Swiping down from the top edge goes to QS (or shade if in split shade mode).
+                swipeDownFromTop(pointerCount = 1) to quickSettingsIfSingleShade,
+                swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade,
+
+                // Swiping down, not from the edge, always navigates to the shade scene.
+                swipeDown(pointerCount = 1) to Scenes.Shade,
+                swipeDown(pointerCount = 2) to Scenes.Shade,
+            )
+            .filterValues { it != null }
+            .mapValues { checkNotNull(it.value) }
     }
 
-    /** The key of the scene we should switch to when swiping left. */
-    val leftDestinationSceneKey: StateFlow<SceneKey?> =
-        communalInteractor.isCommunalAvailable
-            .map { available -> if (available) Scenes.Communal else null }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = null,
-            )
+    private fun swipeDownFromTop(pointerCount: Int): Swipe {
+        return Swipe(
+            SwipeDirection.Down,
+            fromSource = Edge.Top,
+            pointerCount = pointerCount,
+        )
+    }
 
-    /** The key of the scene we should switch to when swiping down from the top edge. */
-    val downFromTopEdgeDestinationSceneKey: StateFlow<SceneKey?> =
-        shadeInteractor.isSplitShade
-            .map { isSplitShade -> Scenes.QuickSettings.takeUnless { isSplitShade } }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = null,
-            )
+    private fun swipeDown(pointerCount: Int): Swipe {
+        return Swipe(
+            SwipeDirection.Down,
+            pointerCount = pointerCount,
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
index 978e71e..b7f7b06 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
@@ -81,6 +81,8 @@
 
     val notificationAlpha: Flow<Float> = keyguardAlpha
 
+    val shortcutsAlpha: Flow<Float> = keyguardAlpha
+
     val notificationTranslationX: Flow<Float> =
         keyguardTranslationX.map { it.value }.filterNotNull()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 91c86df..9d0ea5e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -19,6 +19,7 @@
 import static android.view.InputDevice.SOURCE_TOUCHPAD;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
 
+import static com.android.systemui.Flags.edgebackGestureHandlerGetRunningTasksBackground;
 import static com.android.systemui.classifier.Classifier.BACK_GESTURE;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
@@ -54,7 +55,6 @@
 import android.view.IWindowManager;
 import android.view.InputDevice;
 import android.view.InputEvent;
-import android.view.InputMonitor;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -104,6 +104,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -151,7 +152,12 @@
     private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
         public void onTaskStackChanged() {
-            mGestureBlockingActivityRunning = isGestureBlockingActivityRunning();
+            if (edgebackGestureHandlerGetRunningTasksBackground()) {
+                mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
+                        isGestureBlockingActivityRunning()));
+            } else {
+                mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
+            }
         }
         @Override
         public void onTaskCreated(int taskId, ComponentName componentName) {
@@ -241,6 +247,8 @@
 
     private final PointF mDownPoint = new PointF();
     private final PointF mEndPoint = new PointF();
+    private AtomicBoolean mGestureBlockingActivityRunning = new AtomicBoolean();
+
     private boolean mThresholdCrossed = false;
     private boolean mAllowGesture = false;
     private boolean mLogGesture = false;
@@ -256,7 +264,6 @@
     private boolean mIsEnabled;
     private boolean mIsNavBarShownTransiently;
     private boolean mIsBackGestureAllowed;
-    private boolean mGestureBlockingActivityRunning;
     private boolean mIsNewBackAffordanceEnabled;
     private boolean mIsTrackpadGestureFeaturesEnabled;
     private boolean mIsTrackpadThreeFingerSwipe;
@@ -1017,7 +1024,7 @@
             mInRejectedExclusion = false;
             boolean isWithinInsets = isWithinInsets((int) ev.getX(), (int) ev.getY());
             boolean isBackAllowedCommon = !mDisabledForQuickstep && mIsBackGestureAllowed
-                    && !mGestureBlockingActivityRunning
+                    && !mGestureBlockingActivityRunning.get()
                     && !QuickStepContract.isBackGestureDisabled(mSysUiFlags,
                             mIsTrackpadThreeFingerSwipe)
                     && !isTrackpadScroll(mIsTrackpadGestureFeaturesEnabled, ev);
@@ -1053,8 +1060,8 @@
                     curTime, curTimeStr, mAllowGesture, mIsTrackpadThreeFingerSwipe,
                     mIsOnLeftEdge, mDeferSetIsOnLeftEdge, mIsBackGestureAllowed,
                     QuickStepContract.isBackGestureDisabled(mSysUiFlags,
-                            mIsTrackpadThreeFingerSwipe),
-                    mDisabledForQuickstep, mGestureBlockingActivityRunning, mIsInPip, mDisplaySize,
+                            mIsTrackpadThreeFingerSwipe), mDisabledForQuickstep,
+                    mGestureBlockingActivityRunning.get(), mIsInPip, mDisplaySize,
                     mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion));
         } else if (mAllowGesture || mLogGesture) {
             if (!mThresholdCrossed) {
@@ -1236,7 +1243,7 @@
         pw.println("  mIsBackGestureAllowed=" + mIsBackGestureAllowed);
         pw.println("  mIsGestureHandlingEnabled=" + mIsGestureHandlingEnabled);
         pw.println("  mIsNavBarShownTransiently=" + mIsNavBarShownTransiently);
-        pw.println("  mGestureBlockingActivityRunning=" + mGestureBlockingActivityRunning);
+        pw.println("  mGestureBlockingActivityRunning=" + mGestureBlockingActivityRunning.get());
         pw.println("  mAllowGesture=" + mAllowGesture);
         pw.println("  mUseMLModel=" + mUseMLModel);
         pw.println("  mDisabledForQuickstep=" + mDisabledForQuickstep);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 2440651..cd65119 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -38,6 +38,7 @@
 import com.android.systemui.settings.brightness.BrightnessController;
 import com.android.systemui.settings.brightness.BrightnessMirrorHandler;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
@@ -90,9 +91,11 @@
             FalsingManager falsingManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             SplitShadeStateController splitShadeStateController,
-            SceneContainerFlags sceneContainerFlags) {
+            SceneContainerFlags sceneContainerFlags,
+            VibratorHelper vibratorHelper) {
         super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost,
-                metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController);
+                metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController,
+                vibratorHelper);
         mTunerService = tunerService;
         mQsCustomizerController = qsCustomizerController;
         mQsTileRevealControllerFactory = qsTileRevealControllerFactory;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 975c871..5e12b9d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -39,6 +39,7 @@
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileViewImpl;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.animation.DisappearParameters;
@@ -87,6 +88,8 @@
 
     private SplitShadeStateController mSplitShadeStateController;
 
+    private final VibratorHelper mVibratorHelper;
+
     @VisibleForTesting
     protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
             new QSPanel.OnConfigurationChangedListener() {
@@ -144,7 +147,8 @@
             UiEventLogger uiEventLogger,
             QSLogger qsLogger,
             DumpManager dumpManager,
-            SplitShadeStateController splitShadeStateController
+            SplitShadeStateController splitShadeStateController,
+            VibratorHelper vibratorHelper
     ) {
         super(view);
         mHost = host;
@@ -158,6 +162,7 @@
         mSplitShadeStateController = splitShadeStateController;
         mShouldUseSplitNotificationShade =
                 mSplitShadeStateController.shouldUseSplitNotificationShade(getResources());
+        mVibratorHelper = vibratorHelper;
     }
 
     @Override
@@ -300,7 +305,8 @@
     }
 
     private void addTile(final QSTile tile, boolean collapsedView) {
-        final QSTileViewImpl tileView = new QSTileViewImpl(getContext(), collapsedView);
+        final QSTileViewImpl tileView = new QSTileViewImpl(
+                getContext(), collapsedView, mVibratorHelper);
         final TileRecord r = new TileRecord(tile, tileView);
         // TODO(b/250618218): Remove the QSLogger in QSTileViewImpl once we know the root cause of
         // b/250618218.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index a8e88da..05bb088 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -32,6 +32,7 @@
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.util.leak.RotationUtils;
 
@@ -56,10 +57,11 @@
             @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA)
                     Provider<Boolean> usingCollapsedLandscapeMediaProvider,
             MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
-            DumpManager dumpManager, SplitShadeStateController splitShadeStateController
+            DumpManager dumpManager, SplitShadeStateController splitShadeStateController,
+            VibratorHelper vibratorHelper
     ) {
         super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
-                uiEventLogger, qsLogger, dumpManager, splitShadeStateController);
+                uiEventLogger, qsLogger, dumpManager, splitShadeStateController, vibratorHelper);
         mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSLongPressProperties.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSLongPressProperties.kt
new file mode 100644
index 0000000..a2ded6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSLongPressProperties.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tileimpl
+
+/**
+ * List of properties that define the state of a tile during a long-press gesture.
+ *
+ * These properties are used during animation if a tile supports a long-press action.
+ */
+data class QSLongPressProperties(
+    var xScale: Float,
+    var yScale: Float,
+    var cornerRadius: Float,
+    var backgroundColor: Int,
+    var labelColor: Int,
+    var secondaryLabelColor: Int,
+    var chevronColor: Int,
+    var overlayColor: Int,
+    var iconColor: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 6cc682a..63963de 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -26,6 +26,7 @@
 import android.graphics.Color
 import android.graphics.PorterDuff
 import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.LayerDrawable
 import android.graphics.drawable.RippleDrawable
 import android.os.Trace
@@ -36,6 +37,7 @@
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewConfiguration
 import android.view.ViewGroup
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
@@ -48,9 +50,12 @@
 import com.android.app.tracing.traceSection
 import com.android.settingslib.Utils
 import com.android.systemui.Flags
+import com.android.systemui.Flags.quickSettingsVisualHapticsLongpress
 import com.android.systemui.FontSizeUtils
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
+import com.android.systemui.haptics.qs.QSLongPressEffect
+import com.android.systemui.haptics.qs.QSLongPressEffectViewBinder
 import com.android.systemui.plugins.qs.QSIconView
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.qs.QSTile.AdapterState
@@ -58,12 +63,15 @@
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.children
 import java.util.Objects
 
 private const val TAG = "QSTileViewImpl"
 open class QSTileViewImpl @JvmOverloads constructor(
     context: Context,
-    private val collapsed: Boolean = false
+    private val collapsed: Boolean = false,
+    private val vibratorHelper: VibratorHelper? = null,
 ) : QSTileView(context), HeightOverrideable, LaunchableView {
 
     companion object {
@@ -163,6 +171,7 @@
     private var lastStateDescription: CharSequence? = null
     private var tileState = false
     private var lastState = INVALID
+    private var lastIconTint = 0
     private val launchableViewDelegate = LaunchableViewDelegate(
         this,
         superSetVisibility = { super.setVisibility(it) },
@@ -171,6 +180,12 @@
 
     private val locInScreen = IntArray(2)
 
+    /** Visuo-haptic long-press effects */
+    private var longPressEffect: QSLongPressEffect? = null
+    private var initialLongPressProperties: QSLongPressProperties? = null
+    private var finalLongPressProperties: QSLongPressProperties? = null
+    private val colorEvaluator = ArgbEvaluator.getInstance()
+
     init {
         val typedValue = TypedValue()
         if (!getContext().theme.resolveAttribute(R.attr.isQsTheme, typedValue, true)) {
@@ -339,6 +354,9 @@
                     true
                 }
         )
+        if (quickSettingsVisualHapticsLongpress()) {
+            isHapticFeedbackEnabled = false // Haptics will be handled by the [QSLongPressEffect]
+        }
     }
 
     private fun init(
@@ -589,6 +607,27 @@
 
         lastState = state.state
         lastDisabledByPolicy = state.disabledByPolicy
+        lastIconTint = icon.getColor(state)
+
+        // Long-press effects
+        if (quickSettingsVisualHapticsLongpress()){
+            if (state.handlesLongClick) {
+                // initialize the long-press effect and set it as the touch listener
+                showRippleEffect = false
+                initializeLongPressEffect()
+                setOnTouchListener(longPressEffect)
+                QSLongPressEffectViewBinder.bind(this, longPressEffect)
+            } else {
+                // Long-press effects might have been enabled before but the new state does not
+                // handle a long-press. In this case, we go back to the behaviour of a regular tile
+                // and clean-up the resources
+                showRippleEffect = isClickable
+                setOnTouchListener(null)
+                longPressEffect = null
+                initialLongPressProperties = null
+                finalLongPressProperties = null
+            }
+        }
     }
 
     private fun setAllColors(
@@ -709,6 +748,140 @@
         }
     }
 
+    override fun onActivityLaunchAnimationEnd() = resetLongPressEffectProperties()
+
+    fun updateLongPressEffectProperties(effectProgress: Float) {
+        if (!isLongClickable) return
+        setAllColors(
+            colorEvaluator.evaluate(
+                effectProgress,
+                initialLongPressProperties?.backgroundColor ?: 0,
+                finalLongPressProperties?.backgroundColor ?: 0,
+            ) as Int,
+            colorEvaluator.evaluate(
+                effectProgress,
+                initialLongPressProperties?.labelColor ?: 0,
+                finalLongPressProperties?.labelColor ?: 0,
+            ) as Int,
+            colorEvaluator.evaluate(
+                effectProgress,
+                initialLongPressProperties?.secondaryLabelColor ?: 0,
+                finalLongPressProperties?.secondaryLabelColor ?: 0,
+            ) as Int,
+            colorEvaluator.evaluate(
+                effectProgress,
+                initialLongPressProperties?.chevronColor ?: 0,
+                finalLongPressProperties?.chevronColor ?: 0,
+            ) as Int,
+            colorEvaluator.evaluate(
+                effectProgress,
+                initialLongPressProperties?.overlayColor ?: 0,
+                finalLongPressProperties?.overlayColor ?: 0,
+            ) as Int,
+        )
+        icon.setTint(
+            icon.mIcon as ImageView,
+            colorEvaluator.evaluate(
+                effectProgress,
+                initialLongPressProperties?.iconColor ?: 0,
+                finalLongPressProperties?.iconColor ?: 0,
+            ) as Int,
+        )
+
+        val newScaleX =
+            interpolateFloat(
+                effectProgress,
+                initialLongPressProperties?.xScale ?: 1f,
+                finalLongPressProperties?.xScale ?: 1f,
+            )
+        val newScaleY =
+            interpolateFloat(
+                effectProgress,
+                initialLongPressProperties?.xScale ?: 1f,
+                finalLongPressProperties?.xScale ?: 1f,
+            )
+        val newRadius =
+            interpolateFloat(
+                effectProgress,
+                initialLongPressProperties?.cornerRadius ?: 0f,
+                finalLongPressProperties?.cornerRadius ?: 0f,
+            )
+        scaleX = newScaleX
+        scaleY = newScaleY
+        for (child in children) {
+            child.scaleX = 1f / newScaleX
+            child.scaleY = 1f / newScaleY
+        }
+        changeCornerRadius(newRadius)
+    }
+
+    private fun interpolateFloat(fraction: Float, start: Float, end: Float): Float =
+        start + fraction * (end - start)
+
+    private fun resetLongPressEffectProperties() {
+        scaleY = 1f
+        scaleX = 1f
+        for (child in children) {
+            child.scaleY = 1f
+            child.scaleX = 1f
+        }
+        changeCornerRadius(resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat())
+        setAllColors(
+            getBackgroundColorForState(lastState, lastDisabledByPolicy),
+            getLabelColorForState(lastState, lastDisabledByPolicy),
+            getSecondaryLabelColorForState(lastState, lastDisabledByPolicy),
+            getChevronColorForState(lastState, lastDisabledByPolicy),
+            getOverlayColorForState(lastState),
+        )
+        icon.setTint(icon.mIcon as ImageView, lastIconTint)
+    }
+
+    private fun initializeLongPressEffect() {
+        initializeLongPressProperties()
+        longPressEffect =
+            QSLongPressEffect(
+                vibratorHelper,
+                ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout(),
+            )
+    }
+
+    private fun initializeLongPressProperties() {
+        initialLongPressProperties =
+            QSLongPressProperties(
+                /* xScale= */1f,
+                /* yScale= */1f,
+                resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat(),
+                getBackgroundColorForState(lastState),
+                getLabelColorForState(lastState),
+                getSecondaryLabelColorForState(lastState),
+                getChevronColorForState(lastState),
+                getOverlayColorForState(lastState),
+                lastIconTint,
+            )
+
+        finalLongPressProperties =
+            QSLongPressProperties(
+                /* xScale= */1.1f,
+                /* yScale= */1.2f,
+                resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat() - 20,
+                getBackgroundColorForState(Tile.STATE_ACTIVE),
+                getLabelColorForState(Tile.STATE_ACTIVE),
+                getSecondaryLabelColorForState(Tile.STATE_ACTIVE),
+                getChevronColorForState(Tile.STATE_ACTIVE),
+                getOverlayColorForState(Tile.STATE_ACTIVE),
+                Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive),
+            )
+    }
+
+    private fun changeCornerRadius(radius: Float) {
+        for (i in 0 until backgroundDrawable.numberOfLayers) {
+            val layer = backgroundDrawable.getDrawable(i)
+            if (layer is GradientDrawable) {
+                layer.cornerRadius = radius
+            }
+        }
+    }
+
     @VisibleForTesting
     internal fun getCurrentColors(): List<Int> = listOf(
             backgroundColor,
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 0d9b702..8a84496 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -19,6 +19,7 @@
 import android.app.NotificationManager
 import android.content.Context
 import android.content.Intent
+import android.content.pm.LauncherApps
 import android.content.res.Resources
 import android.net.Uri
 import android.os.Handler
@@ -93,6 +94,10 @@
             }
             ACTION_STOP,
             ACTION_STOP_NOTIF -> {
+                // ViewCapture needs to save it's data before it is disabled, or else the data will
+                // be lost. This is expected to change in the near future, and when that happens
+                // this line should be removed.
+                getSystemService(LauncherApps::class.java)?.saveViewCaptureData()
                 TraceUtils.traceStop(contentResolver)
             }
             ACTION_SHARE -> {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index a490fe2..451fd67 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
@@ -40,17 +41,17 @@
     shadeInteractor: ShadeInteractor,
 ) {
     val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
-        shadeInteractor.isSplitShade
-            .map { isSplitShade -> destinationScenes(isSplitShade = isSplitShade) }
+        shadeInteractor.shadeMode
+            .map { shadeMode -> destinationScenes(shadeMode = shadeMode) }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = destinationScenes(isSplitShade = shadeInteractor.isSplitShade.value)
+                initialValue = destinationScenes(shadeMode = shadeInteractor.shadeMode.value)
             )
 
-    private fun destinationScenes(isSplitShade: Boolean): Map<UserAction, UserActionResult> {
+    private fun destinationScenes(shadeMode: ShadeMode): Map<UserAction, UserActionResult> {
         return buildMap {
-            if (!isSplitShade) {
+            if (shadeMode == ShadeMode.Single) {
                 this[
                     Swipe(
                         pointerCount = 2,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
index f01e9be..1d9b755 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
@@ -21,7 +21,6 @@
 import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.Rect
-import android.graphics.drawable.Drawable
 import android.util.Log
 import android.view.Display
 import android.view.KeyEvent
@@ -32,6 +31,7 @@
 import android.view.WindowInsets
 import android.window.OnBackInvokedCallback
 import android.window.OnBackInvokedDispatcher
+import androidx.appcompat.content.res.AppCompatResources
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.res.R
@@ -42,20 +42,15 @@
  * Legacy implementation of screenshot view methods. Just proxies the calls down into the original
  * ScreenshotView.
  */
-class LegacyScreenshotViewProxy(context: Context, private val logger: UiEventLogger) :
+class LegacyScreenshotViewProxy(private val context: Context, private val logger: UiEventLogger) :
     ScreenshotViewProxy {
     override val view: ScreenshotView =
         LayoutInflater.from(context).inflate(R.layout.screenshot, null) as ScreenshotView
     override val screenshotPreview: View
-
     override var defaultDisplay: Int = Display.DEFAULT_DISPLAY
         set(value) {
             view.setDefaultDisplay(value)
         }
-    override var defaultTimeoutMillis: Long = 6000
-        set(value) {
-            view.setDefaultTimeoutMillis(value)
-        }
     override var flags: FeatureFlags? = null
         set(value) {
             view.setFlags(value)
@@ -70,7 +65,16 @@
         }
     override var screenshot: ScreenshotData? = null
         set(value) {
-            view.setScreenshot(value)
+            field = value
+            value?.let {
+                val badgeBg =
+                    AppCompatResources.getDrawable(context, R.drawable.overlay_badge_background)
+                val user = it.userHandle
+                if (badgeBg != null && user != null) {
+                    view.badgeScreenshot(context.packageManager.getUserBadgedIcon(badgeBg, user))
+                }
+                view.setScreenshot(it)
+            }
         }
 
     override val isAttachedToWindow
@@ -95,8 +99,6 @@
     override fun updateInsets(insets: WindowInsets) = view.updateInsets(insets)
     override fun updateOrientation(insets: WindowInsets) = view.updateOrientation(insets)
 
-    override fun badgeScreenshot(userBadgedIcon: Drawable) = view.badgeScreenshot(userBadgedIcon)
-
     override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator =
         view.createScreenshotDropInAnimation(screenRect, showFlash)
 
@@ -130,14 +132,17 @@
         response: ScrollCaptureResponse,
         screenBitmap: Bitmap,
         newScreenshot: Bitmap,
-        screenshotTakenInPortrait: Boolean
-    ) =
+        screenshotTakenInPortrait: Boolean,
+        onTransitionPrepared: Runnable,
+    ) {
         view.prepareScrollingTransition(
             response,
             screenBitmap,
             newScreenshot,
             screenshotTakenInPortrait
         )
+        view.post { onTransitionPrepared.run() }
+    }
 
     override fun startLongScreenshotTransition(
         transitionDestination: Rect,
@@ -155,10 +160,19 @@
 
     override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
 
-    override fun getViewTreeObserver(): ViewTreeObserver = view.viewTreeObserver
-
-    override fun post(runnable: Runnable) {
-        view.post(runnable)
+    override fun prepareEntranceAnimation(runnable: Runnable) {
+        view.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    if (LogConfig.DEBUG_WINDOW) {
+                        Log.d(TAG, "onPreDraw: startAnimation")
+                    }
+                    view.viewTreeObserver.removeOnPreDrawListener(this)
+                    runnable.run()
+                    return true
+                }
+            }
+        )
     }
 
     private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 6bab956..9aaed9e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -228,7 +228,7 @@
     // From WizardManagerHelper.java
     private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
 
-    private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
+    static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
 
     private final WindowContext mContext;
     private final FeatureFlags mFlags;
@@ -460,7 +460,7 @@
 
         attachWindow();
 
-        boolean showFlash = true;
+        boolean showFlash;
         if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
             if (screenshot.getScreenBounds() != null
                     && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(),
@@ -472,15 +472,14 @@
                 screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(),
                         screenshot.getBitmap().getHeight()));
             }
+        } else {
+            showFlash = true;
         }
 
-        prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> {
-            mMessageContainerController.onScreenshotTaken(screenshot);
-        });
+        mViewProxy.prepareEntranceAnimation(
+                () -> startAnimation(screenshot.getScreenBounds(), showFlash,
+                        () -> mMessageContainerController.onScreenshotTaken(screenshot)));
 
-        mViewProxy.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
-                mContext.getDrawable(R.drawable.overlay_badge_background),
-                screenshot.getUserHandle()));
         mViewProxy.setScreenshot(screenshot);
 
         // ignore system bar insets for the purpose of window layout
@@ -598,7 +597,6 @@
         });
         mViewProxy.setFlags(mFlags);
         mViewProxy.setDefaultDisplay(mDisplayId);
-        mViewProxy.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
 
         if (DEBUG_WINDOW) {
             Log.d(TAG, "setContentView: " + mViewProxy.getView());
@@ -606,22 +604,6 @@
         setContentView(mViewProxy.getView());
     }
 
-    private void prepareAnimation(Rect screenRect, boolean showFlash,
-            Runnable onAnimationComplete) {
-        mViewProxy.getViewTreeObserver().addOnPreDrawListener(
-                new ViewTreeObserver.OnPreDrawListener() {
-                    @Override
-                    public boolean onPreDraw() {
-                        if (DEBUG_WINDOW) {
-                            Log.d(TAG, "onPreDraw: startAnimation");
-                        }
-                        mViewProxy.getViewTreeObserver().removeOnPreDrawListener(this);
-                        startAnimation(screenRect, showFlash, onAnimationComplete);
-                        return true;
-                    }
-                });
-    }
-
     private void enqueueScrollCaptureRequest(UserHandle owner) {
         // Wait until this window is attached to request because it is
         // the reference used to locate the target window (below).
@@ -706,10 +688,14 @@
                 Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId,
                         new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
 
-                mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
-                        mScreenshotTakenInPortrait);
-                // delay starting scroll capture to make sure the scrim is up before the app moves
-                mViewProxy.post(() -> runBatchScrollCapture(response, owner));
+                if (newScreenshot != null) {
+                    // delay starting scroll capture to make sure scrim is up before the app moves
+                    mViewProxy.prepareScrollingTransition(
+                            response, mScreenBitmap, newScreenshot, mScreenshotTakenInPortrait,
+                            () -> runBatchScrollCapture(response, owner));
+                } else {
+                    Log.wtf(TAG, "failed to capture current screenshot for scroll transition");
+                }
             });
         } catch (InterruptedException | ExecutionException e) {
             Log.e(TAG, "requestScrollCapture failed", e);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 8a8766d..1c5a8a1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -26,6 +26,7 @@
 import static com.android.systemui.screenshot.LogConfig.DEBUG_UI;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
 import static com.android.systemui.screenshot.LogConfig.logTag;
+import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS;
 
 import static java.util.Objects.requireNonNull;
 
@@ -33,6 +34,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.BroadcastOptions;
 import android.app.Notification;
@@ -168,7 +170,6 @@
     private ScreenshotData mScreenshotData;
 
     private final InteractionJankMonitor mInteractionJankMonitor;
-    private long mDefaultTimeoutOfTimeoutHandler;
     private FeatureFlags mFlags;
     private final Bundle mInteractiveBroadcastOption;
 
@@ -244,10 +245,6 @@
         return InteractionJankMonitor.getInstance();
     }
 
-    void setDefaultTimeoutMillis(long timeout) {
-        mDefaultTimeoutOfTimeoutHandler = timeout;
-    }
-
     public void hideScrollChip() {
         mScrollChip.setVisibility(View.GONE);
     }
@@ -755,7 +752,7 @@
                         InteractionJankMonitor.Configuration.Builder.withView(
                                         CUJ_TAKE_SCREENSHOT, mScreenshotStatic)
                                 .setTag("Actions")
-                                .setTimeout(mDefaultTimeoutOfTimeoutHandler);
+                                .setTimeout(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
                 mInteractionJankMonitor.begin(builder);
             }
         });
@@ -781,7 +778,7 @@
         return animator;
     }
 
-    void badgeScreenshot(Drawable badge) {
+    void badgeScreenshot(@Nullable Drawable badge) {
         mScreenshotBadge.setImageDrawable(badge);
         mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
index d5c7f95..d019660 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
@@ -21,11 +21,9 @@
 import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.Rect
-import android.graphics.drawable.Drawable
 import android.view.ScrollCaptureResponse
 import android.view.View
 import android.view.ViewGroup
-import android.view.ViewTreeObserver
 import android.view.WindowInsets
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.flags.FeatureFlags
@@ -36,7 +34,6 @@
     val screenshotPreview: View
 
     var defaultDisplay: Int
-    var defaultTimeoutMillis: Long
     var flags: FeatureFlags?
     var packageName: String
     var callbacks: ScreenshotView.ScreenshotViewCallback?
@@ -49,7 +46,6 @@
     fun reset()
     fun updateInsets(insets: WindowInsets)
     fun updateOrientation(insets: WindowInsets)
-    fun badgeScreenshot(userBadgedIcon: Drawable)
     fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator
     fun addQuickShareChip(quickShareAction: Notification.Action)
     fun setChipIntents(imageData: ScreenshotController.SavedImageData)
@@ -61,7 +57,8 @@
         response: ScrollCaptureResponse,
         screenBitmap: Bitmap,
         newScreenshot: Bitmap,
-        screenshotTakenInPortrait: Boolean
+        screenshotTakenInPortrait: Boolean,
+        onTransitionPrepared: Runnable,
     )
     fun startLongScreenshotTransition(
         transitionDestination: Rect,
@@ -73,8 +70,7 @@
     fun stopInputListening()
     fun requestFocus()
     fun announceForAccessibility(string: String)
-    fun getViewTreeObserver(): ViewTreeObserver
-    fun post(runnable: Runnable)
+    fun prepareEntranceAnimation(runnable: Runnable)
 
     interface Factory {
         fun getProxy(context: Context, logger: UiEventLogger): ScreenshotViewProxy
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java
index 71c2cb4..5df6c45 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java
@@ -40,7 +40,7 @@
     private final Context mContext;
 
     private Runnable mOnTimeout;
-    private int mDefaultTimeout = DEFAULT_TIMEOUT_MILLIS;
+    int mDefaultTimeout = DEFAULT_TIMEOUT_MILLIS;
 
     @Inject
     public TimeoutHandler(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index cdb520f..8b791de 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1540,13 +1540,13 @@
         mKeyguardBottomArea = keyguardBottomArea;
     }
 
-    @Override
+    /** Sets a listener to be notified when the shade starts opening or finishes closing. */
     public void setOpenCloseListener(OpenCloseListener openCloseListener) {
         SceneContainerFlag.assertInLegacyMode();
         mOpenCloseListener = openCloseListener;
     }
 
-    @Override
+    /** Sets a listener to be notified when touch tracking begins. */
     public void setTrackingStartedListener(TrackingStartedListener trackingStartedListener) {
         mTrackingStartedListener = trackingStartedListener;
     }
@@ -1753,10 +1753,11 @@
     }
 
     private void updateKeyguardStatusViewAlignment(boolean animate) {
+        boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
         if (migrateClocksToBlueprint()) {
+            mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
             return;
         }
-        boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
         ConstraintLayout layout = mNotificationContainerParent;
         mKeyguardStatusViewController.updateAlignment(
                 layout, mSplitShadeEnabled, shouldBeCentered, animate);
@@ -2603,7 +2604,6 @@
         return maxHeight;
     }
 
-    @Override
     public boolean isExpandingOrCollapsing() {
         float lockscreenExpansionProgress = mQsController.getLockscreenShadeDragProgress();
         return mIsExpandingOrCollapsing
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt b/packages/SystemUI/src/com/android/systemui/shade/OpenCloseListener.kt
similarity index 66%
rename from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
rename to packages/SystemUI/src/com/android/systemui/shade/OpenCloseListener.kt
index 98a9e93..108dd478 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/OpenCloseListener.kt
@@ -14,16 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.credentialmanager.ui.screens
+package com.android.systemui.shade
 
-import androidx.activity.result.IntentSenderRequest
+/** Listens for when shade begins opening or finishes closing. */
+interface OpenCloseListener {
+    /** Called when the shade finishes closing. */
+    fun onClosingFinished()
 
-sealed class UiState {
-    data object CredentialScreen : UiState()
-
-    data class CredentialSelected(
-        val intentSenderRequest: IntentSenderRequest?
-    ) : UiState()
-
-    data object Cancel : UiState()
+    /** Called when the shade starts opening. */
+    fun onOpenStarted()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 27168a7..177c3db 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -89,8 +89,7 @@
 
     override fun isShadeFullyOpen(): Boolean = shadeInteractor.isAnyFullyExpanded.value
 
-    override fun isExpandingOrCollapsing(): Boolean =
-        shadeInteractor.anyExpansion.value > 0f && shadeInteractor.anyExpansion.value < 1f
+    override fun isExpandingOrCollapsing(): Boolean = shadeInteractor.isUserInteracting.value
 
     override fun instantExpandShade() {
         // Do nothing
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt
index a9ba6f9..859fce5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt
@@ -27,9 +27,6 @@
      */
     @Deprecated("Use ShadeInteractor instead") fun expandToNotifications()
 
-    /** Returns whether the shade is expanding or collapsing itself or quick settings. */
-    val isExpandingOrCollapsing: Boolean
-
     /**
      * Returns whether the shade height is greater than zero (i.e. partially or fully expanded),
      * there is a HUN, the shade is animating, or the shade is instantly expanding.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index 07236d1..de21a73 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -31,9 +31,6 @@
  * @see NotificationPanelViewController
  */
 interface ShadeViewController {
-    /** Returns whether the shade is expanding or collapsing itself or quick settings. */
-    val isExpandingOrCollapsing: Boolean
-
     /**
      * Returns whether the shade height is greater than zero or the shade is expecting a synthesized
      * down event.
@@ -52,15 +49,9 @@
     /** Returns whether the shade's top level view is enabled. */
     val isViewEnabled: Boolean
 
-    /** Sets a listener to be notified when the shade starts opening or finishes closing. */
-    fun setOpenCloseListener(openCloseListener: OpenCloseListener)
-
     /** Returns whether status bar icons should be hidden when the shade is expanded. */
     fun shouldHideStatusBarIconsWhenExpanded(): Boolean
 
-    /** Sets a listener to be notified when touch tracking begins. */
-    fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener)
-
     /**
      * Disables the shade header.
      *
@@ -250,17 +241,3 @@
     /** Return the fraction of the shade that's expanded, when in lockscreen. */
     val lockscreenShadeDragProgress: Float
 }
-
-/** Listens for when touch tracking begins. */
-interface TrackingStartedListener {
-    fun onTrackingStarted()
-}
-
-/** Listens for when shade begins opening or finishes closing. */
-interface OpenCloseListener {
-    /** Called when the shade finishes closing. */
-    fun onClosingFinished()
-
-    /** Called when the shade starts opening. */
-    fun onOpenStarted()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
index 6e03686..b67156f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -34,7 +34,6 @@
     ShadeLockscreenInteractor,
     PanelExpansionInteractor {
     override fun expandToNotifications() {}
-    override val isExpandingOrCollapsing: Boolean = false
     override val isExpanded: Boolean = false
     override val isPanelExpanded: Boolean = false
     override fun animateCollapseQs(fullyCollapse: Boolean) {}
@@ -43,10 +42,8 @@
     override val isFullyCollapsed: Boolean = false
     override val isTracking: Boolean = false
     override val isViewEnabled: Boolean = false
-    override fun setOpenCloseListener(openCloseListener: OpenCloseListener) {}
     override fun shouldHideStatusBarIconsWhenExpanded() = false
     override fun blockExpansionForCurrentTouch() {}
-    override fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener) {}
     override fun disableHeader(state1: Int, state2: Int, animated: Boolean) {}
     override fun startExpandLatencyTracking() {}
     override fun startBouncerPreHideAnimation() {}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt b/packages/SystemUI/src/com/android/systemui/shade/TrackingStartedListener.kt
similarity index 65%
copy from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
copy to packages/SystemUI/src/com/android/systemui/shade/TrackingStartedListener.kt
index 98a9e93..3803c27 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/TrackingStartedListener.kt
@@ -14,16 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.credentialmanager.ui.screens
+package com.android.systemui.shade
 
-import androidx.activity.result.IntentSenderRequest
-
-sealed class UiState {
-    data object CredentialScreen : UiState()
-
-    data class CredentialSelected(
-        val intentSenderRequest: IntentSenderRequest?
-    ) : UiState()
-
-    data object Cancel : UiState()
+/** Listens for when touch tracking begins. */
+interface TrackingStartedListener {
+    fun onTrackingStarted()
 }
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 e050c0b..5c79e1e 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
@@ -16,6 +16,7 @@
 package com.android.systemui.shade.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -100,8 +101,7 @@
     @Deprecated("Use ShadeInteractor.isQsBypassingShade instead")
     val legacyExpandImmediate: StateFlow<Boolean>
 
-    /** Whether the current configuration requires the split shade to be shown. */
-    val isSplitShade: StateFlow<Boolean>
+    val shadeMode: StateFlow<ShadeMode>
 
     /** True when QS is taking up the entire screen, i.e. fully expanded on a non-unfolded phone. */
     @Deprecated("Use ShadeInteractor instead") val legacyQsFullscreen: StateFlow<Boolean>
@@ -109,7 +109,7 @@
     /** NPVC.mClosing as a flow. */
     @Deprecated("Use ShadeAnimationInteractor instead") val legacyIsClosing: StateFlow<Boolean>
 
-    fun setSplitShade(isSplitShade: Boolean)
+    fun setShadeMode(mode: ShadeMode)
 
     /** Sets whether a closing animation is happening. */
     @Deprecated("Use ShadeAnimationInteractor instead") fun setLegacyIsClosing(isClosing: Boolean)
@@ -219,11 +219,11 @@
     @Deprecated("Use ShadeInteractor instead")
     override val legacyQsFullscreen: StateFlow<Boolean> = _legacyQsFullscreen.asStateFlow()
 
-    val _isSplitShade = MutableStateFlow(false)
-    override val isSplitShade: StateFlow<Boolean> = _isSplitShade.asStateFlow()
+    val _shadeMode = MutableStateFlow<ShadeMode>(ShadeMode.Single)
+    override val shadeMode: StateFlow<ShadeMode> = _shadeMode.asStateFlow()
 
-    override fun setSplitShade(isSplitShade: Boolean) {
-        _isSplitShade.value = isSplitShade
+    override fun setShadeMode(shadeMode: ShadeMode) {
+        _shadeMode.value = shadeMode
     }
 
     override fun setLegacyQsFullscreen(legacyQsFullscreen: Boolean) {
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 ad3fbe5..bc60c83 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade.domain.interactor
 
+import com.android.systemui.shade.shared.model.ShadeMode
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
@@ -103,8 +104,7 @@
      */
     val isUserInteractingWithQs: Flow<Boolean>
 
-    /** Whether the current configuration requires the split shade to be shown. */
-    val isSplitShade: StateFlow<Boolean>
+    val shadeMode: StateFlow<ShadeMode>
 }
 
 fun createAnyExpansionFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index 57a36b5..e9bb4c6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -42,5 +43,5 @@
     override val isUserInteracting: StateFlow<Boolean> = inactiveFlowBoolean
     override val isShadeTouchable: Flow<Boolean> = inactiveFlowBoolean
     override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
-    override val isSplitShade: StateFlow<Boolean> = inactiveFlowBoolean
+    override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index 3bccd2b..6414af3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -99,7 +100,7 @@
     override val isUserInteractingWithQs: Flow<Boolean> =
         userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion)
 
-    override val isSplitShade: StateFlow<Boolean> = repository.isSplitShade
+    override val shadeMode: StateFlow<ShadeMode> = repository.shadeMode
 
     /**
      * Return a flow for whether a user is interacting with an expandable shade component using
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index ad8c029..7785eda 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -108,7 +109,7 @@
     override val isUserInteractingWithQs: Flow<Boolean> =
         sceneBasedInteracting(sceneInteractor, Scenes.QuickSettings)
 
-    override val isSplitShade: StateFlow<Boolean> = shadeRepository.isSplitShade
+    override val shadeMode: StateFlow<ShadeMode> = shadeRepository.shadeMode
 
     /**
      * Returns a flow that uses scene transition progress to and from a scene that is pulled down
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index 1f78ae8..3d9337e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -38,8 +38,6 @@
         changeToShadeScene()
     }
 
-    override val isExpandingOrCollapsing = shadeInteractor.isUserInteracting.value
-
     override val isExpanded = shadeInteractor.isAnyExpanded.value
 
     override fun startBouncerPreHideAnimation() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index 334908e..11ce818 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -41,17 +42,25 @@
 ) : CoreStartable {
 
     override fun start() {
-        hydrateSplitShade()
+        hydrateShadeMode()
     }
 
-    private fun hydrateSplitShade() {
+    private fun hydrateShadeMode() {
         applicationScope.launch {
             configurationRepository.onAnyConfigurationChange
                 // Force initial collection.
                 .onStart { emit(Unit) }
                 .map { applicationContext.resources }
                 .map { resources -> controller.shouldUseSplitNotificationShade(resources) }
-                .collect { isSplitShade -> shadeRepository.setSplitShade(isSplitShade) }
+                .collect { isSplitShade ->
+                    shadeRepository.setShadeMode(
+                        if (isSplitShade) {
+                            ShadeMode.Split
+                        } else {
+                            ShadeMode.Single
+                        }
+                    )
+                }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
new file mode 100644
index 0000000..3451eaf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.shared.model
+
+/** Enumerates all known modes of operation of the shade. */
+sealed interface ShadeMode {
+
+    /**
+     * The single or "accordion" shade where the QS and notification parts are in two vertically
+     * stacked panels and the user can swipe up and down to expand or collapse between the two
+     * parts.
+     */
+    data object Single : ShadeMode
+
+    /**
+     * The split shade where, on large screens and unfolded foldables, the QS and notification parts
+     * are placed side-by-side and expand/collapse as a single panel.
+     */
+    data object Split : ShadeMode
+
+    /**
+     * The dual shade where the QS and notification parts each have their own independently
+     * expandable/collapsible panel on either side of the large screen / unfolded device or sharing
+     * a space on a small screen or folded device.
+     */
+    data object Dual : ShadeMode
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 8084a6f..ea549f2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
@@ -64,12 +65,12 @@
         combine(
                 deviceEntryInteractor.isUnlocked,
                 deviceEntryInteractor.canSwipeToEnter,
-                shadeInteractor.isSplitShade,
-            ) { isUnlocked, canSwipeToDismiss, isSplitShade ->
+                shadeInteractor.shadeMode,
+            ) { isUnlocked, canSwipeToDismiss, shadeMode ->
                 destinationScenes(
                     isUnlocked = isUnlocked,
                     canSwipeToDismiss = canSwipeToDismiss,
-                    isSplitShade = isSplitShade,
+                    shadeMode = shadeMode,
                 )
             }
             .stateIn(
@@ -79,7 +80,7 @@
                     destinationScenes(
                         isUnlocked = deviceEntryInteractor.isUnlocked.value,
                         canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
-                        isSplitShade = shadeInteractor.isSplitShade.value,
+                        shadeMode = shadeInteractor.shadeMode.value,
                     ),
             )
 
@@ -96,8 +97,7 @@
                 initialValue = false
             )
 
-    /** Whether the current configuration requires the split shade to be shown. */
-    val isSplitShade: StateFlow<Boolean> = shadeInteractor.isSplitShade
+    val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
 
     /** Notifies that some content in the shade was clicked. */
     fun onContentClicked() = deviceEntryInteractor.attemptDeviceEntry()
@@ -119,7 +119,7 @@
     private fun destinationScenes(
         isUnlocked: Boolean,
         canSwipeToDismiss: Boolean?,
-        isSplitShade: Boolean,
+        shadeMode: ShadeMode,
     ): Map<UserAction, UserActionResult> {
         val up =
             when {
@@ -128,7 +128,7 @@
                 else -> Scenes.Lockscreen
             }
 
-        val down = if (isSplitShade) null else Scenes.QuickSettings
+        val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
 
         return buildMap {
             this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 6155348..5171a5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -39,8 +39,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.animation.ShadeInterpolation;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.flags.RefactorFlag;
 import com.android.systemui.res.R;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
@@ -95,8 +93,6 @@
     private float mCornerAnimationDistance;
     private float mActualWidth = -1;
     private int mMaxIconsOnLockscreen;
-    private final RefactorFlag mSensitiveRevealAnim =
-            RefactorFlag.forView(Flags.SENSITIVE_REVEAL_ANIM);
     private boolean mCanModifyColorOfNotifications;
     private boolean mCanInteract;
     private NotificationStackScrollLayout mHostLayout;
@@ -266,7 +262,7 @@
         }
 
         final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
-        if (mSensitiveRevealAnim.isEnabled() && viewState.hidden) {
+        if (viewState.hidden) {
             // if the shelf is hidden, position it at the end of the stack (plus the clip
             // padding), such that when it appears animated, it will smoothly move in from the
             // bottom, without jump cutting any notifications
@@ -398,10 +394,6 @@
         //  find the first view that doesn't overlap with the shelf
         int notGoneIndex = 0;
         int colorOfViewBeforeLast = NO_COLOR;
-        boolean backgroundForceHidden = false;
-        if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) {
-            backgroundForceHidden = true;
-        }
         int colorTwoBefore = NO_COLOR;
         int previousColor = NO_COLOR;
         float transitionAmount = 0.0f;
@@ -429,8 +421,7 @@
                     expandingAnimated, isLastChild, shelfClipStart);
 
             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
-            if ((!mSensitiveRevealAnim.isEnabled() && ((isLastChild && !child.isInShelf())
-                    || backgroundForceHidden)) || aboveShelf) {
+            if (aboveShelf) {
                 notificationClipEnd = shelfStart + getIntrinsicHeight();
             } else {
                 notificationClipEnd = shelfStart - mPaddingBetweenElements;
@@ -440,8 +431,7 @@
 
             // If the current row is an ExpandableNotificationRow, update its color, roundedness,
             // and icon state.
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow expandableRow) {
                 numViewsInShelf += inShelfAmount;
                 int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint();
                 if (viewStart >= shelfStart && mNotGoneIndex == -1) {
@@ -471,16 +461,8 @@
                 notGoneIndex++;
             }
 
-            if (child instanceof ActivatableNotificationView) {
-                ActivatableNotificationView anv = (ActivatableNotificationView) child;
-                // Because we show whole notifications on the lockscreen, the bottom notification is
-                // always "just about to enter the shelf" by normal scrolling rules.  This is fine
-                // if the shelf is visible, but if the shelf is hidden, it causes incorrect curling.
-                // notificationClipEnd handles the discrepancy between a visible and hidden shelf,
-                // so we use that when on the keyguard (and while animating away) to reduce curling.
-                final float keyguardSafeShelfStart = !mSensitiveRevealAnim.isEnabled()
-                        && mAmbientState.isOnKeyguard() ? notificationClipEnd : shelfStart;
-                updateCornerRoundnessOnScroll(anv, viewStart, keyguardSafeShelfStart);
+            if (child instanceof ActivatableNotificationView anv) {
+                updateCornerRoundnessOnScroll(anv, viewStart, shelfStart);
             }
         }
 
@@ -519,11 +501,10 @@
         mShelfIcons.applyIconStates();
         for (int i = 0; i < getHostLayoutChildCount(); i++) {
             View child = getHostLayoutChildAt(i);
-            if (!(child instanceof ExpandableNotificationRow)
+            if (!(child instanceof ExpandableNotificationRow row)
                     || child.getVisibility() == GONE) {
                 continue;
             }
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
             updateContinuousClipping(row);
         }
         boolean hideBackground = isHidden;
@@ -613,8 +594,7 @@
     private void clipTransientViews() {
         for (int i = 0; i < getHostLayoutTransientViewCount(); i++) {
             View transientView = getHostLayoutTransientView(i);
-            if (transientView instanceof ExpandableView) {
-                ExpandableView transientExpandableView = (ExpandableView) transientView;
+            if (transientView instanceof ExpandableView transientExpandableView) {
                 updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1);
             }
         }
@@ -871,10 +851,9 @@
     }
 
     private void setIconTransformationAmount(ExpandableView view, float transitionAmount) {
-        if (!(view instanceof ExpandableNotificationRow)) {
+        if (!(view instanceof ExpandableNotificationRow row)) {
             return;
         }
-        ExpandableNotificationRow row = (ExpandableNotificationRow) view;
         StatusBarIconView icon = row.getShelfIcon();
         NotificationIconContainer.IconState iconState = getIconState(icon);
         if (iconState == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 5f3a83a..c05c3c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -278,8 +278,6 @@
     private OnExpandClickListener mOnExpandClickListener;
     private View.OnClickListener mOnFeedbackClickListener;
     private Path mExpandingClipPath;
-    private final RefactorFlag mInlineReplyAnimation =
-            RefactorFlag.forView(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION);
 
     private static boolean shouldSimulateSlowMeasure() {
         return Compile.IS_DEBUG && RefactorFlag.forView(
@@ -355,7 +353,7 @@
                     nowExpanded = !isExpanded();
                     setUserExpanded(nowExpanded);
                 }
-                notifyHeightChanged(true);
+                notifyHeightChanged(/* needsAnimation= */ true);
                 mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
                 mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_EXPANDER, nowExpanded);
             }
@@ -778,7 +776,7 @@
             mChildrenContainer.updateGroupOverflow();
         }
         if (intrinsicBefore != getIntrinsicHeight()) {
-            notifyHeightChanged(false  /* needsAnimation */);
+            notifyHeightChanged(/* needsAnimation= */ false);
         }
         if (isHeadsUp) {
             mMustStayOnScreen = true;
@@ -826,7 +824,7 @@
             if (mChildrenContainer != null) {
                 mChildrenContainer.setHeaderVisibleAmount(headerVisibleAmount);
             }
-            notifyHeightChanged(false /* needsAnimation */);
+            notifyHeightChanged(/* needsAnimation= */ false);
         }
     }
 
@@ -1088,7 +1086,7 @@
         boolean wasAboveShelf = isAboveShelf();
         mIsPinned = pinned;
         if (intrinsicHeight != getIntrinsicHeight()) {
-            notifyHeightChanged(false /* needsAnimation */);
+            notifyHeightChanged(/* needsAnimation= */ false);
         }
         if (pinned) {
             setAnimationRunning(true);
@@ -2611,7 +2609,7 @@
         onExpansionChanged(true /* userAction */, wasExpanded);
         if (!wasExpanded && isExpanded()
                 && getActualHeight() != getIntrinsicHeight()) {
-            notifyHeightChanged(true /* needsAnimation */);
+            notifyHeightChanged(/* needsAnimation= */ true);
         }
     }
 
@@ -2623,7 +2621,7 @@
             if (mIsSummaryWithChildren) {
                 mChildrenContainer.onExpansionChanged();
             }
-            notifyHeightChanged(false /* needsAnimation */);
+            notifyHeightChanged(/* needsAnimation= */ false);
         }
         updateShelfIconColor();
     }
@@ -2661,7 +2659,7 @@
         if (expand != mIsSystemExpanded) {
             final boolean wasExpanded = isExpanded();
             mIsSystemExpanded = expand;
-            notifyHeightChanged(false /* needsAnimation */);
+            notifyHeightChanged(/* needsAnimation= */ false);
             onExpansionChanged(false /* userAction */, wasExpanded);
             if (mIsSummaryWithChildren) {
                 mChildrenContainer.updateGroupOverflow();
@@ -2680,7 +2678,7 @@
                 if (mIsSummaryWithChildren) {
                     mChildrenContainer.updateGroupOverflow();
                 }
-                notifyHeightChanged(false /* needsAnimation */);
+                notifyHeightChanged(/* needsAnimation= */ false);
             }
             if (isAboveShelf() != wasAboveShelf) {
                 mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
@@ -2837,7 +2835,7 @@
         super.onLayout(changed, left, top, right, bottom);
         if (intrinsicBefore != getIntrinsicHeight()
                 && (intrinsicBefore != 0 || getActualHeight() > 0)) {
-            notifyHeightChanged(true  /* needsAnimation */);
+            notifyHeightChanged(/* needsAnimation= */ true);
         }
         if (mMenuRow != null && mMenuRow.getMenuView() != null) {
             mMenuRow.onParentHeightUpdate();
@@ -2880,8 +2878,7 @@
         mSensitiveHiddenInGeneral = hideSensitive;
         int intrinsicAfter = getIntrinsicHeight();
         if (intrinsicBefore != intrinsicAfter) {
-            boolean needsAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
-            notifyHeightChanged(needsAnimation);
+            notifyHeightChanged(/* needsAnimation= */ true);
         }
     }
 
@@ -3018,7 +3015,7 @@
         if (isChildInGroup()) {
             mGroupExpansionManager.setGroupExpanded(mEntry, true);
         }
-        notifyHeightChanged(false /* needsAnimation */);
+        notifyHeightChanged(/* needsAnimation= */ false);
     }
 
     public void setChildrenExpanded(boolean expanded, boolean animate) {
@@ -3241,13 +3238,8 @@
             mGuts.setActualHeight(height);
             return;
         }
-        int contentHeight = Math.max(getMinHeight(), height);
         for (NotificationContentView l : mLayouts) {
-            if (mInlineReplyAnimation.isEnabled()) {
-                l.setContentHeight(height);
-            } else {
-                l.setContentHeight(contentHeight);
-            }
+            l.setContentHeight(height);
         }
         if (mIsSummaryWithChildren) {
             mChildrenContainer.setActualHeight(height);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 137e1b2..8a3e7e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -699,8 +699,7 @@
         int hint;
         if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
             hint = getViewHeight(VISIBLE_TYPE_HEADSUP);
-            if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isAnimatingAppearance()
-                    && mHeadsUpRemoteInputController.isFocusAnimationFlagActive()) {
+            if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isAnimatingAppearance()) {
                 // While the RemoteInputView is animating its appearance, it should be allowed
                 // to overlap the hint, therefore no space is reserved for the hint during the
                 // appearance animation of the RemoteInputView
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index b205071..77e9425 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -87,7 +87,6 @@
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.flags.RefactorFlag;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.res.R;
@@ -197,8 +196,6 @@
      */
     private Set<Integer> mDebugTextUsedYPositions;
     private final boolean mDebugRemoveAnimation;
-    private final boolean mSensitiveRevealAnimEndabled;
-    private final RefactorFlag mAnimatedInsets;
     private int mContentHeight;
     private float mIntrinsicContentHeight;
     private int mPaddingBetweenElements;
@@ -619,9 +616,6 @@
                 Flags.LOCKSCREEN_ENABLE_LANDSCAPE);
         mDebugLines = mFeatureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
         mDebugRemoveAnimation = mFeatureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
-        mSensitiveRevealAnimEndabled = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
-        mAnimatedInsets =
-                new RefactorFlag(mFeatureFlags, Flags.ANIMATED_NOTIFICATION_SHADE_INSETS);
         mSectionsManager = Dependency.get(NotificationSectionsManager.class);
         mScreenOffAnimationController =
                 Dependency.get(ScreenOffAnimationController.class);
@@ -656,9 +650,7 @@
         mGroupMembershipManager = Dependency.get(GroupMembershipManager.class);
         mGroupExpansionManager = Dependency.get(GroupExpansionManager.class);
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-        if (mAnimatedInsets.isEnabled()) {
-            setWindowInsetsAnimationCallback(mInsetsCallback);
-        }
+        setWindowInsetsAnimationCallback(mInsetsCallback);
     }
 
     /**
@@ -1734,11 +1726,7 @@
             return;
         }
         mForcedScroll = v;
-        if (mAnimatedInsets.isEnabled()) {
-            updateForcedScroll();
-        } else {
-            scrollTo(v);
-        }
+        updateForcedScroll();
     }
 
     public boolean scrollTo(View v) {
@@ -1783,31 +1771,15 @@
 
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        if (!mAnimatedInsets.isEnabled()) {
-            mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom;
-        }
         mWaterfallTopInset = 0;
         final DisplayCutout cutout = insets.getDisplayCutout();
         if (cutout != null) {
             mWaterfallTopInset = cutout.getWaterfallInsets().top;
         }
-        if (mAnimatedInsets.isEnabled() && !mIsInsetAnimationRunning) {
+        if (!mIsInsetAnimationRunning) {
             // update bottom inset e.g. after rotation
             updateBottomInset(insets);
         }
-        if (!mAnimatedInsets.isEnabled()) {
-            int range = getScrollRange();
-            if (mOwnScrollY > range) {
-                // HACK: We're repeatedly getting staggered insets here while the IME is
-                // animating away. To work around that we'll wait until things have settled.
-                removeCallbacks(mReclamp);
-                postDelayed(mReclamp, 50);
-            } else if (mForcedScroll != null) {
-                // The scroll was requested before we got the actual inset - in case we need
-                // to scroll up some more do so now.
-                scrollTo(mForcedScroll);
-            }
-        }
         return insets;
     }
 
@@ -2576,7 +2548,7 @@
             return;
         }
         child.setOnHeightChangedListener(null);
-        if (child instanceof ExpandableNotificationRow && mSensitiveRevealAnimEndabled) {
+        if (child instanceof ExpandableNotificationRow) {
             NotificationEntry entry = ((ExpandableNotificationRow) child).getEntry();
             entry.removeOnSensitivityChangedListener(mOnChildSensitivityChangedListener);
         }
@@ -2872,7 +2844,7 @@
     private void onViewAddedInternal(ExpandableView child) {
         updateHideSensitiveForChild(child);
         child.setOnHeightChangedListener(mOnChildHeightChangedListener);
-        if (child instanceof ExpandableNotificationRow && mSensitiveRevealAnimEndabled) {
+        if (child instanceof ExpandableNotificationRow) {
             NotificationEntry entry = ((ExpandableNotificationRow) child).getEntry();
             entry.addOnSensitivityChangedListener(mOnChildSensitivityChangedListener);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index e2e13a1..ab9ecab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -313,7 +313,7 @@
                 mHeadsUpManager.unpinAll(true /* userUnpinned */);
                 mMetricsLogger.count("panel_open", 1);
             } else if (!mQsController.getExpanded()
-                    && !mShadeViewController.isExpandingOrCollapsing()) {
+                    && !mShadeController.isExpandingOrCollapsing()) {
                 mShadeController.animateExpandQs();
                 mMetricsLogger.count("panel_open_qs", 1);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index ba89d4a..7dd328a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -579,7 +579,7 @@
         final boolean hideBouncerOverDream =
                 mDreamOverlayStateController.isOverlayActive()
                         && (mShadeLockscreenInteractor.isExpanded()
-                        || mShadeLockscreenInteractor.isExpandingOrCollapsing());
+                        || mShadeController.get().isExpandingOrCollapsing());
 
         final boolean isUserTrackingStarted =
                 event.getFraction() != EXPANSION_HIDDEN && event.getTracking();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 5bced93..9633cb0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -135,7 +135,6 @@
     @Nullable
     private RevealParams mRevealParams;
     private Rect mContentBackgroundBounds;
-    private boolean mIsFocusAnimationFlagActive;
     private boolean mIsAnimatingAppearance = false;
 
     // TODO(b/193539698): move these to a Controller
@@ -432,7 +431,7 @@
         // case to prevent flicker.
         if (!mRemoved) {
             ViewGroup parent = (ViewGroup) getParent();
-            if (animate && parent != null && mIsFocusAnimationFlagActive) {
+            if (animate && parent != null) {
 
                 ViewGroup grandParent = (ViewGroup) parent.getParent();
                 View actionsContainer = getActionsContainerLayout();
@@ -497,8 +496,7 @@
     }
 
     private void setTopMargin(int topMargin) {
-        if (!(getLayoutParams() instanceof FrameLayout.LayoutParams)) return;
-        final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
+        if (!(getLayoutParams() instanceof FrameLayout.LayoutParams layoutParams)) return;
         layoutParams.topMargin = topMargin;
         setLayoutParams(layoutParams);
     }
@@ -608,24 +606,10 @@
     }
 
     /**
-     * Sets whether the feature flag for the revised inline reply animation is active or not.
-     * @param active
-     */
-    public void setIsFocusAnimationFlagActive(boolean active) {
-        mIsFocusAnimationFlagActive = active;
-    }
-
-    /**
      * Focuses the RemoteInputView and animates its appearance
      */
     public void focusAnimated() {
-        if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE
-                && mRevealParams != null) {
-            android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this);
-            animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-            animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-            animator.start();
-        } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) {
+        if (getVisibility() != VISIBLE) {
             mIsAnimatingAppearance = true;
             setAlpha(0f);
             Animator focusAnimator = getFocusAnimator(getActionsContainerLayout());
@@ -680,37 +664,19 @@
     }
 
     private void reset() {
-        if (mIsFocusAnimationFlagActive) {
-            mProgressBar.setVisibility(INVISIBLE);
-            mResetting = true;
-            mSending = false;
-            mController.removeSpinning(mEntry.getKey(), mToken);
-            onDefocus(true /* animate */, false /* logClose */, () -> {
-                mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
-                mEditText.getText().clear();
-                mEditText.setEnabled(isAggregatedVisible());
-                mSendButton.setVisibility(VISIBLE);
-                updateSendButton();
-                setAttachment(null);
-                mResetting = false;
-            });
-            return;
-        }
-
+        mProgressBar.setVisibility(INVISIBLE);
         mResetting = true;
         mSending = false;
-        mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
-
-        mEditText.getText().clear();
-        mEditText.setEnabled(isAggregatedVisible());
-        mSendButton.setVisibility(VISIBLE);
-        mProgressBar.setVisibility(INVISIBLE);
         mController.removeSpinning(mEntry.getKey(), mToken);
-        updateSendButton();
-        onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */);
-        setAttachment(null);
-
-        mResetting = false;
+        onDefocus(true /* animate */, false /* logClose */, () -> {
+            mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
+            mEditText.getText().clear();
+            mEditText.setEnabled(isAggregatedVisible());
+            mSendButton.setVisibility(VISIBLE);
+            updateSendButton();
+            setAttachment(null);
+            mResetting = false;
+        });
     }
 
     @Override
@@ -854,7 +820,7 @@
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
-        if (mIsFocusAnimationFlagActive) setPivotY(getMeasuredHeight());
+        setPivotY(getMeasuredHeight());
         if (mContentBackgroundBounds != null) {
             mContentBackground.setBounds(mContentBackgroundBounds);
         }
@@ -1015,9 +981,9 @@
 
         private RemoteInputView mRemoteInputView;
         boolean mShowImeOnInputConnection;
-        private LightBarController mLightBarController;
+        private final LightBarController mLightBarController;
         private InputMethodManager mInputMethodManager;
-        private ArraySet<String> mSupportedMimes = new ArraySet<>();
+        private final ArraySet<String> mSupportedMimes = new ArraySet<>();
         UserHandle mUser;
 
         public RemoteEditText(Context context, AttributeSet attrs) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
index 6c0d433..bfee9ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
@@ -32,7 +32,6 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.res.R
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION
 import com.android.systemui.statusbar.NotificationRemoteInputManager
 import com.android.systemui.statusbar.RemoteInputController
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -64,8 +63,6 @@
 
     var revealParams: RevealParams?
 
-    val isFocusAnimationFlagActive: Boolean
-
     /**
      * Sets the smart reply that should be inserted in the remote input, or `null` if the user is
      * not editing a smart reply.
@@ -155,9 +152,6 @@
 
     override val isActive: Boolean get() = view.isActive
 
-    override val isFocusAnimationFlagActive: Boolean
-        get() = mFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION)
-
     override fun bind() {
         if (isBound) return
         isBound = true
@@ -168,7 +162,6 @@
             view.setSupportedMimeTypes(it.allowedDataTypes)
         }
         view.setRevealParameters(revealParams)
-        view.setIsFocusAnimationFlagActive(isFocusAnimationFlagActive)
 
         view.addOnEditTextFocusChangedListener(onFocusChangeListener)
         view.addOnSendRemoteInputListener(onSendRemoteInputListener)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
index 71df8e5..1bceee9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
@@ -36,3 +36,7 @@
     /** Current media state is unknown yet. */
     data object Unknown : MediaDeviceSession
 }
+
+/** Returns true when the audio is playing for the [MediaDeviceSession]. */
+fun MediaDeviceSession.isPlaying(): Boolean =
+    this is MediaDeviceSession.Active && playbackState?.isActive == true
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index 37661b5..d49cb1e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
 import javax.inject.Inject
@@ -110,9 +111,6 @@
                 null,
             )
 
-    private fun MediaDeviceSession.isPlaying(): Boolean =
-        this is MediaDeviceSession.Active && playbackState?.isActive == true
-
     fun onBarClick(expandable: Expandable) {
         actionsInteractor.onBarClick(mediaDeviceSession.value, expandable)
         volumePanelViewModel.dismissPanel()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index faf7434..532e517 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -21,6 +21,7 @@
 import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
 import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.res.R
 import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor
@@ -54,14 +55,6 @@
             AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_ringer,
             AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_alarm,
         )
-    private val mutedIconsByStream =
-        mapOf(
-            AudioStream(AudioManager.STREAM_MUSIC) to R.drawable.ic_volume_off,
-            AudioStream(AudioManager.STREAM_VOICE_CALL) to R.drawable.ic_volume_off,
-            AudioStream(AudioManager.STREAM_RING) to R.drawable.ic_volume_off,
-            AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_off,
-            AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_off,
-        )
     private val labelsByStream =
         mapOf(
             AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_music,
@@ -74,6 +67,8 @@
         mapOf(
             AudioStream(AudioManager.STREAM_NOTIFICATION) to
                 R.string.stream_notification_unavailable,
+            AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm_unavailable,
+            AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_media_unavailable,
         )
 
     private var value = 0f
@@ -81,8 +76,9 @@
         combine(
                 audioVolumeInteractor.getAudioStream(audioStream),
                 audioVolumeInteractor.canChangeVolume(audioStream),
-            ) { model, isEnabled ->
-                model.toState(value, isEnabled)
+                audioVolumeInteractor.ringerMode,
+            ) { model, isEnabled, ringerMode ->
+                model.toState(value, isEnabled, ringerMode)
             }
             .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState)
 
@@ -100,7 +96,11 @@
         }
     }
 
-    private fun AudioStreamModel.toState(value: Float, isEnabled: Boolean): State {
+    private fun AudioStreamModel.toState(
+        value: Float,
+        isEnabled: Boolean,
+        ringerMode: RingerMode,
+    ): State {
         return State(
             value =
                 volumeSliderInteractor.processVolumeToValue(
@@ -110,7 +110,7 @@
                     isMuted,
                 ),
             valueRange = volumeSliderInteractor.displayValueRange,
-            icon = getIcon(this),
+            icon = getIcon(ringerMode),
             label = labelsByStream[audioStream]?.let(context::getString)
                     ?: error("No label for the stream: $audioStream"),
             disabledMessage = disabledTextByStream[audioStream]?.let(context::getString),
@@ -119,14 +119,31 @@
         )
     }
 
-    private fun getIcon(model: AudioStreamModel): Icon {
-        val isMutedOrNoVolume = model.isMuted || model.volume == model.minVolume
+    private fun AudioStreamModel.getIcon(ringerMode: RingerMode): Icon {
+        val isMutedOrNoVolume = isMuted || volume == minVolume
         val iconRes =
             if (isMutedOrNoVolume) {
-                mutedIconsByStream
+                when (audioStream.value) {
+                    AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_off
+                    AudioManager.STREAM_VOICE_CALL -> R.drawable.ic_volume_off
+                    AudioManager.STREAM_RING ->
+                        if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
+                            R.drawable.ic_volume_ringer_vibrate
+                        } else {
+                            R.drawable.ic_volume_off
+                        }
+                    AudioManager.STREAM_NOTIFICATION ->
+                        if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
+                            R.drawable.ic_volume_ringer_vibrate
+                        } else {
+                            R.drawable.ic_volume_off
+                        }
+                    AudioManager.STREAM_ALARM -> R.drawable.ic_volume_off
+                    else -> null
+                }
             } else {
-                iconsByStream
-            }[audioStream]
+                iconsByStream[audioStream]
+            }
                 ?: error("No icon for the stream: $audioStream")
         return Icon.Resource(iconRes, null)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
index 2824323..aaee24b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
@@ -18,6 +18,8 @@
 
 import android.media.AudioManager
 import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
 import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel
@@ -28,12 +30,17 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.launch
 
 /**
  * Controls the behaviour of the whole audio
@@ -46,6 +53,7 @@
 constructor(
     @VolumePanelScope private val scope: CoroutineScope,
     castVolumeInteractor: CastVolumeInteractor,
+    mediaOutputInteractor: MediaOutputInteractor,
     private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
     private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
 ) {
@@ -90,4 +98,17 @@
                 remoteSessionsViewModels + streamViewModels
             }
             .stateIn(scope, SharingStarted.Eagerly, emptyList())
+
+    private val mutableIsExpanded = MutableSharedFlow<Boolean>()
+
+    val isExpanded: StateFlow<Boolean> =
+        merge(
+                mutableIsExpanded.onStart { emit(false) },
+                mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() },
+            )
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    fun onExpandedChanged(isExpanded: Boolean) {
+        scope.launch { mutableIsExpanded.emit(isExpanded) }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
index dd428f5..ccdcee5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
@@ -32,7 +32,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -57,7 +56,6 @@
 
     @Before
     public void setUp() throws Exception {
-        mFeatureFlags.setDefault(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION);
         mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
         mDependency.injectMockDependency(NotificationMediaManager.class);
         allowTestableLooperAsMainThread();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index e796303..701b703 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -401,7 +401,7 @@
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
                     Pair("Enter PIN", "PIN is required after lockdown"),
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    Pair("Enter PIN", "Update will install when device not in use"),
+                    Pair("Enter PIN", "PIN required for additional security"),
                 LockPatternUtils.StrongAuthTracker
                     .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
                     Pair(
@@ -439,7 +439,7 @@
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
                     Pair("Enter PIN", "PIN is required after lockdown"),
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    Pair("Enter PIN", "Update will install when device not in use"),
+                    Pair("Enter PIN", "PIN required for additional security"),
                 LockPatternUtils.StrongAuthTracker
                     .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
                     Pair(
@@ -481,7 +481,7 @@
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
                     Pair("Enter PIN", "PIN is required after lockdown"),
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    Pair("Enter PIN", "Update will install when device not in use"),
+                    Pair("Enter PIN", "PIN required for additional security"),
                 LockPatternUtils.StrongAuthTracker
                     .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
                     Pair(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 22a2e93..f252163 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -130,6 +130,24 @@
             assertThat(value()).isEqualTo(LARGE)
         }
 
+    @Test
+    fun isLargeClockVisible_whenLargeClockSize_isTrue() =
+        scope.runTest {
+            fakeSettings.putInt(LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1)
+            keyguardClockRepository.setClockSize(LARGE)
+            var value = collectLastValue(underTest.isLargeClockVisible)
+            assertThat(value()).isEqualTo(true)
+        }
+
+    @Test
+    fun isLargeClockVisible_whenSmallClockSize_isFalse() =
+        scope.runTest {
+            fakeSettings.putInt(LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1)
+            keyguardClockRepository.setClockSize(SMALL)
+            var value = collectLastValue(underTest.isLargeClockVisible)
+            assertThat(value()).isEqualTo(false)
+        }
+
     private fun setupMockClock() {
         whenever(clock.largeClock).thenReturn(largeClock)
         whenever(largeClock.config).thenReturn(clockFaceConfig)
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 bcec6109..b80dcd4 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
@@ -62,6 +62,7 @@
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth
 import kotlin.math.min
+import kotlin.test.assertEquals
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.emptyFlow
@@ -77,7 +78,6 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
-import kotlin.test.assertEquals
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -134,7 +134,12 @@
     private lateinit var lockscreenToPrimaryBouncerTransitionViewModel:
         LockscreenToPrimaryBouncerTransitionViewModel
     @Mock
-    private lateinit var transitionInteractor: KeyguardTransitionInteractor
+    private lateinit var lockscreenToGlanceableHubTransitionViewModel:
+        LockscreenToGlanceableHubTransitionViewModel
+    @Mock
+    private lateinit var glanceableHubToLockscreenTransitionViewModel:
+        GlanceableHubToLockscreenTransitionViewModel
+    @Mock private lateinit var transitionInteractor: KeyguardTransitionInteractor
 
     private lateinit var underTest: KeyguardQuickAffordancesCombinedViewModel
 
@@ -271,6 +276,10 @@
         whenever(lockscreenToOccludedTransitionViewModel.shortcutsAlpha).thenReturn(emptyFlow())
         whenever(lockscreenToPrimaryBouncerTransitionViewModel.shortcutsAlpha)
             .thenReturn(emptyFlow())
+        whenever(lockscreenToGlanceableHubTransitionViewModel.shortcutsAlpha)
+            .thenReturn(emptyFlow())
+        whenever(glanceableHubToLockscreenTransitionViewModel.shortcutsAlpha)
+            .thenReturn(emptyFlow())
         whenever(shadeInteractor.anyExpansion).thenReturn(intendedShadeAlphaMutableStateFlow)
         whenever(transitionInteractor.finishedKeyguardState)
             .thenReturn(intendedFinishedKeyguardStateFlow)
@@ -307,6 +316,8 @@
                 offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
                 primaryBouncerToLockscreenTransitionViewModel =
                     primaryBouncerToLockscreenTransitionViewModel,
+                glanceableHubToLockscreenTransitionViewModel =
+                    glanceableHubToLockscreenTransitionViewModel,
                 lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel,
                 lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
                 lockscreenToDreamingHostedTransitionViewModel =
@@ -316,6 +327,8 @@
                 lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
                 lockscreenToPrimaryBouncerTransitionViewModel =
                     lockscreenToPrimaryBouncerTransitionViewModel,
+                lockscreenToGlanceableHubTransitionViewModel =
+                    lockscreenToGlanceableHubTransitionViewModel,
                 transitionInteractor = transitionInteractor,
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index 65ede89..0101741 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -51,6 +51,7 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 import com.android.systemui.util.animation.DisappearParameters;
 
@@ -100,6 +101,8 @@
     Configuration mConfiguration;
     @Mock
     Runnable mHorizontalLayoutListener;
+    @Mock
+    VibratorHelper mVibratorHelper;
 
     private QSPanelControllerBase<QSPanel> mController;
 
@@ -110,7 +113,8 @@
                 MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
                 DumpManager dumpManager) {
             super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger,
-                    qsLogger, dumpManager, new ResourcesSplitShadeStateController());
+                    qsLogger, dumpManager, new ResourcesSplitShadeStateController(),
+                    mVibratorHelper);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index 85d7d98..916e8dd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
 import com.android.systemui.settings.brightness.BrightnessController
 import com.android.systemui.settings.brightness.BrightnessSliderController
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
 import com.android.systemui.tuner.TunerService
@@ -61,6 +62,7 @@
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock private lateinit var configuration: Configuration
     @Mock private lateinit var pagedTileLayout: PagedTileLayout
+    @Mock private lateinit var vibratorHelper: VibratorHelper
 
     private val sceneContainerFlags = FakeSceneContainerFlags()
 
@@ -101,6 +103,7 @@
             statusBarKeyguardViewManager,
             ResourcesSplitShadeStateController(),
             sceneContainerFlags,
+            vibratorHelper,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 2c14308..71a9a8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.customize.QSCustomizerController
 import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
 import com.android.systemui.util.leak.RotationUtils
 import org.junit.After
@@ -59,6 +60,7 @@
     @Mock private lateinit var tile: QSTile
     @Mock private lateinit var tileLayout: TileLayout
     @Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
+    @Mock private lateinit var vibratorHelper: VibratorHelper
 
     private val uiEventLogger = UiEventLoggerFake()
     private val dumpManager = DumpManager()
@@ -89,7 +91,8 @@
                 metricsLogger,
                 uiEventLogger,
                 qsLogger,
-                dumpManager
+                dumpManager,
+                vibratorHelper,
             )
 
         controller.init()
@@ -157,7 +160,8 @@
         metricsLogger: MetricsLogger,
         uiEventLogger: UiEventLoggerFake,
         qsLogger: QSLogger,
-        dumpManager: DumpManager
+        dumpManager: DumpManager,
+        vibratorHelper: VibratorHelper,
     ) :
         QuickQSPanelController(
             view,
@@ -170,7 +174,8 @@
             uiEventLogger,
             qsLogger,
             dumpManager,
-            ResourcesSplitShadeStateController()
+            ResourcesSplitShadeStateController(),
+            vibratorHelper,
         ) {
 
         private var rotation = RotationUtils.ROTATION_NONE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 42a6924..b114e13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -60,7 +60,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestableContext;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
@@ -104,7 +103,6 @@
                 TestableLooper.get(this),
                 mFeatureFlags);
         mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
-        mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM);
     }
 
     @Test
@@ -186,14 +184,6 @@
     }
 
     @Test
-    public void testSetSensitiveOnNotifRowNotifiesOfHeightChange_withOtherFlagValue()
-            throws Exception {
-        FakeFeatureFlags flags = mFeatureFlags;
-        flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
-        testSetSensitiveOnNotifRowNotifiesOfHeightChange();
-    }
-
-    @Test
     public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception {
         // GIVEN a sensitive notification row that's currently redacted
         ExpandableNotificationRow row = mNotificationTestHelper.createRow();
@@ -210,19 +200,10 @@
         // WHEN the row is set to no longer be sensitive
         row.setSensitive(false, true);
 
-        boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
         // VERIFY that the height change listener is invoked
         assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout());
         assertThat(row.getIntrinsicHeight()).isGreaterThan(0);
-        verify(listener).onHeightChanged(eq(row), eq(expectAnimation));
-    }
-
-    @Test
-    public void testSetSensitiveOnGroupRowNotifiesOfHeightChange_withOtherFlagValue()
-            throws Exception {
-        FakeFeatureFlags flags = mFeatureFlags;
-        flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
-        testSetSensitiveOnGroupRowNotifiesOfHeightChange();
+        verify(listener).onHeightChanged(eq(row), eq(true));
     }
 
     @Test
@@ -242,19 +223,10 @@
         // WHEN the row is set to no longer be sensitive
         group.setSensitive(false, true);
 
-        boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
         // VERIFY that the height change listener is invoked
         assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPrivateLayout());
         assertThat(group.getIntrinsicHeight()).isGreaterThan(0);
-        verify(listener).onHeightChanged(eq(group), eq(expectAnimation));
-    }
-
-    @Test
-    public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange_withOtherFlagValue()
-            throws Exception {
-        FakeFeatureFlags flags = mFeatureFlags;
-        flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
-        testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange();
+        verify(listener).onHeightChanged(eq(group), eq(true));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index b938029..9a7b8ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -10,7 +10,6 @@
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.res.R
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.NotificationShelf
@@ -23,7 +22,6 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
-import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -38,7 +36,6 @@
 @RunWithLooper
 open class NotificationShelfTest : SysuiTestCase() {
 
-    open val useSensitiveReveal: Boolean = false
     private val flags = FakeFeatureFlags()
 
     @Mock private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator
@@ -53,7 +50,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         mDependency.injectTestDependency(FeatureFlags::class.java, flags)
-        flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal)
         val root = FrameLayout(context)
         shelf =
             LayoutInflater.from(root.context)
@@ -335,7 +331,6 @@
     @Test
     fun updateState_withNullLastVisibleBackgroundChild_hideShelf() {
         // GIVEN
-        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -362,7 +357,6 @@
     @Test
     fun updateState_withNullFirstViewInShelf_hideShelf() {
         // GIVEN
-        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -389,7 +383,6 @@
     @Test
     fun updateState_withCollapsedShade_hideShelf() {
         // GIVEN
-        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -416,7 +409,6 @@
     @Test
     fun updateState_withHiddenSectionBeforeShelf_hideShelf() {
         // GIVEN
-        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -476,10 +468,3 @@
         assertEquals(expectedAlpha, shelf.viewState.alpha)
     }
 }
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@RunWithLooper
-class NotificationShelfWithSensitiveRevealTest : NotificationShelfTest() {
-    override val useSensitiveReveal: Boolean = true
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 220305c..13df091 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -165,8 +165,6 @@
         // TODO: Ideally we wouldn't need to set these unless a test actually reads them,
         //  and then we would test both configurations, but currently they are all read
         //  in the constructor.
-        mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM);
-        mFeatureFlags.setDefault(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS);
         mSetFlagsRule.enableFlags(FLAG_NEW_AOD_TRANSITION);
         mFeatureFlags.setDefault(Flags.UNCLEARED_TRANSIENT_HUN_FIX);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 13167b2..c259782 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -71,7 +71,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -455,7 +454,6 @@
     private RemoteInputViewController bindController(
             RemoteInputView view,
             NotificationEntry entry) {
-        mFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
         RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl(
                 view,
                 entry,
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 974e396..5206db4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -95,6 +95,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -292,6 +293,7 @@
         assertEquals(VolumeDialogImpl.PROGRESS_HAPTICS_DISABLED, type);
     }
 
+    @Ignore("Causing breakages so ignoring to resolve, b/329099861")
     @Test
     @EnableFlags(FLAG_HAPTIC_VOLUME_SLIDER)
     public void testVolumeChange_withSliderHaptics_deliversOnProgressChangedHapticsEagerly() {
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 6b604e1..728c67a 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
@@ -18,6 +18,7 @@
 package com.android.systemui.shade.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.shared.model.ShadeMode
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
@@ -60,8 +61,8 @@
 
     override val legacyLockscreenShadeTracking = MutableStateFlow(false)
 
-    private val _isSplitShade = MutableStateFlow(false)
-    override val isSplitShade: StateFlow<Boolean> = _isSplitShade.asStateFlow()
+    private val _shadeMode = MutableStateFlow<ShadeMode>(ShadeMode.Single)
+    override val shadeMode: StateFlow<ShadeMode> = _shadeMode.asStateFlow()
 
     @Deprecated("Use ShadeInteractor instead")
     override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) {
@@ -136,8 +137,8 @@
         _legacyShadeExpansion.value = expandedFraction
     }
 
-    override fun setSplitShade(isSplitShade: Boolean) {
-        _isSplitShade.value = isSplitShade
+    override fun setShadeMode(shadeMode: ShadeMode) {
+        _shadeMode.value = shadeMode
     }
 }
 
diff --git a/packages/Tethering/OWNERS b/packages/Tethering/OWNERS
deleted file mode 100644
index aa87958..0000000
--- a/packages/Tethering/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-include /services/core/java/com/android/server/net/OWNERS
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 10e6ed4..3239175 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -59,6 +59,8 @@
 import android.hardware.camera2.extension.SizeList;
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.PhysicalCaptureResultInfo;
+import android.hardware.camera2.params.ColorSpaceProfiles;
+import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.hardware.camera2.utils.SurfaceUtils;
 import android.media.Image;
 import android.media.ImageReader;
@@ -1228,7 +1230,6 @@
 
             return null;
         }
-
     }
 
     private class CaptureCallbackStub implements SessionProcessorImpl.CaptureCallback {
@@ -1585,11 +1586,13 @@
             Camera2SessionConfigImpl sessionConfig;
 
             if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                int outputsColorSpace = getColorSpaceFromOutputSurfaces(previewSurface,
+                        imageCaptureSurface, postviewSurface);
                 OutputSurfaceConfigurationImplStub outputSurfaceConfigs =
                         new OutputSurfaceConfigurationImplStub(mOutputPreviewSurfaceImpl,
                         // Image Analysis Output is currently only supported in CameraX
                         mOutputImageCaptureSurfaceImpl, null /*imageAnalysisSurfaceConfig*/,
-                        mOutputPostviewSurfaceImpl);
+                        mOutputPostviewSurfaceImpl, outputsColorSpace);
 
                 sessionConfig = mSessionProcessor.initSession(cameraId,
                         getCharacteristicsMap(charsMapNative),
@@ -1616,6 +1619,11 @@
                 }
                 ret.outputConfigs.add(entry);
             }
+            if (Flags.extension10Bit() && EFV_SUPPORTED) {
+                ret.colorSpace = sessionConfig.getColorSpace();
+            } else {
+                ret.colorSpace = ColorSpaceProfiles.UNSPECIFIED;
+            }
             ret.sessionTemplateId = sessionConfig.getSessionTemplateId();
             ret.sessionType = -1;
             if (LATENCY_IMPROVEMENTS_SUPPORTED) {
@@ -1720,6 +1728,24 @@
         public void binderDied() {
             mSessionProcessor.deInitSession();
         }
+
+        // Get the color space of the output configurations. All of the OutputSurfaces
+        // can be assumed to have the same color space so return the color space
+        // of any non-null OutputSurface
+        private int getColorSpaceFromOutputSurfaces(OutputSurface previewSurface,
+                OutputSurface imageCaptureSurface, OutputSurface postviewSurface) {
+            int colorSpace = ColorSpaceProfiles.UNSPECIFIED;
+
+            if (previewSurface.surface != null) {
+                colorSpace = previewSurface.colorSpace;
+            } else if (imageCaptureSurface.surface != null) {
+                colorSpace = imageCaptureSurface.colorSpace;
+            } else if (postviewSurface.surface != null) {
+                colorSpace = postviewSurface.colorSpace;
+            }
+
+            return colorSpace;
+        }
     }
 
     private class OutputSurfaceConfigurationImplStub implements OutputSurfaceConfigurationImpl {
@@ -1727,6 +1753,17 @@
         private OutputSurfaceImpl mOutputImageCaptureSurfaceImpl;
         private OutputSurfaceImpl mOutputImageAnalysisSurfaceImpl;
         private OutputSurfaceImpl mOutputPostviewSurfaceImpl;
+        private int mColorSpace;
+
+        public OutputSurfaceConfigurationImplStub(OutputSurfaceImpl previewOutput,
+                OutputSurfaceImpl imageCaptureOutput, OutputSurfaceImpl imageAnalysisOutput,
+                OutputSurfaceImpl postviewOutput, int colorSpace) {
+            mOutputPreviewSurfaceImpl = previewOutput;
+            mOutputImageCaptureSurfaceImpl = imageCaptureOutput;
+            mOutputImageAnalysisSurfaceImpl = imageAnalysisOutput;
+            mOutputPostviewSurfaceImpl = postviewOutput;
+            mColorSpace = colorSpace;
+        }
 
         public OutputSurfaceConfigurationImplStub(OutputSurfaceImpl previewOutput,
                 OutputSurfaceImpl imageCaptureOutput, OutputSurfaceImpl imageAnalysisOutput,
@@ -1735,6 +1772,7 @@
             mOutputImageCaptureSurfaceImpl = imageCaptureOutput;
             mOutputImageAnalysisSurfaceImpl = imageAnalysisOutput;
             mOutputPostviewSurfaceImpl = postviewOutput;
+            mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
         }
 
         @Override
@@ -1756,6 +1794,11 @@
         public OutputSurfaceImpl getPostviewOutputSurface() {
             return mOutputPostviewSurfaceImpl;
         }
+
+        @Override
+        public int getColorSpace() {
+            return mColorSpace;
+        }
     }
 
     private class OutputSurfaceImplStub implements OutputSurfaceImpl {
@@ -1764,11 +1807,10 @@
         private final int mImageFormat;
         private final int mDataspace;
         private final long mUsage;
+        private final long mDynamicRangeProfile;
 
         public OutputSurfaceImplStub(OutputSurface outputSurface) {
             mSurface = outputSurface.surface;
-            mSize = new Size(outputSurface.size.width, outputSurface.size.height);
-            mImageFormat = outputSurface.imageFormat;
             if (mSurface != null) {
                 mDataspace = SurfaceUtils.getSurfaceDataspace(mSurface);
                 mUsage = SurfaceUtils.getSurfaceUsage(mSurface);
@@ -1776,6 +1818,9 @@
                 mDataspace = -1;
                 mUsage = 0;
             }
+            mDynamicRangeProfile = outputSurface.dynamicRangeProfile;
+            mSize = new Size(outputSurface.size.width, outputSurface.size.height);
+            mImageFormat = outputSurface.imageFormat;
         }
 
         @Override
@@ -1802,6 +1847,12 @@
         public long getUsage() {
             return mUsage;
         }
+
+        @Override
+        public long getDynamicRangeProfile() {
+            return mDynamicRangeProfile;
+        }
+
     }
 
     private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub implements
@@ -2531,6 +2582,11 @@
 
     private static CameraOutputConfig getCameraOutputConfig(Camera2OutputConfigImpl output) {
         CameraOutputConfig ret = new CameraOutputConfig();
+        if (Flags.extension10Bit() && EFV_SUPPORTED) {
+            ret.dynamicRangeProfile = output.getDynamicRangeProfile();
+        } else {
+            ret.dynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+        }
         ret.outputId = new OutputConfigId();
         ret.outputId.id = output.getId();
         ret.physicalCameraId = output.getPhysicalCameraId();
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index af47ed2..73584154 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -611,12 +611,12 @@
         if (svcConnTracingEnabled()) {
             logTraceSvcConn("getWindow", "windowId=" + windowId);
         }
+        int displayId = Display.INVALID_DISPLAY;
+        if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID) {
+            displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId(
+                    mSystemSupport.getCurrentUserIdLocked(), windowId);
+        }
         synchronized (mLock) {
-            int displayId = Display.INVALID_DISPLAY;
-            if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID) {
-                displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowIdLocked(
-                        mSystemSupport.getCurrentUserIdLocked(), windowId);
-            }
             ensureWindowsAvailableTimedLocked(displayId);
 
             if (!hasRightsToCurrentUserLocked()) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 3cbfd42..4be303a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1261,15 +1261,14 @@
             // the computation for performance reasons.
             boolean shouldComputeWindows = false;
             int displayId = event.getDisplayId();
+            final int windowId = event.getWindowId();
+            if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
+                    && displayId == Display.INVALID_DISPLAY) {
+                displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId(
+                        resolvedUserId, windowId);
+                event.setDisplayId(displayId);
+            }
             synchronized (mLock) {
-                final int windowId = event.getWindowId();
-                if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
-                        && displayId == Display.INVALID_DISPLAY) {
-                    displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowIdLocked(
-                            resolvedUserId, windowId);
-                    event.setDisplayId(displayId);
-                }
-
                 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                         && displayId != Display.INVALID_DISPLAY
                         && mA11yWindowManager.isTrackingWindowsLocked(displayId)) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index b818150..8c06bc8 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -2038,8 +2038,11 @@
      * @param windowId The windowId
      * @return The display ID
      */
-    public int getDisplayIdByUserIdAndWindowIdLocked(int userId, int windowId) {
-        final IBinder windowToken = getWindowTokenForUserAndWindowIdLocked(userId, windowId);
+    public int getDisplayIdByUserIdAndWindowId(int userId, int windowId) {
+        final IBinder windowToken;
+        synchronized (mLock) {
+            windowToken = getWindowTokenForUserAndWindowIdLocked(userId, windowId);
+        }
         if (traceWMEnabled()) {
             logTraceWM("getDisplayIdForWindow", "token=" + windowToken);
         }
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index d9e25ef..e13994e 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -525,8 +525,9 @@
             mReceivedPointersDown |= pointerFlag;
             mReceivedPointers[pointerId].set(
                     event.getX(pointerIndex), event.getY(pointerIndex), event.getEventTime());
-
-            mPrimaryPointerId = pointerId;
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                mPrimaryPointerId = pointerId;
+            }
         }
 
         /**
diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
index 82ab098..340bc32 100644
--- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
+++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
@@ -38,7 +38,8 @@
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
-      ]
+      ],
+      "keywords": ["primary-device"]
     },
     {
       "name": "CtsHardwareTestCases",
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 60bfc63..5e6ff55 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4755,6 +4755,7 @@
                         autofillOptions,
                         contentCaptureOptions,
                         app.getDisabledCompatChanges(),
+                        app.getLoggableCompatChanges(),
                         serializedSystemFontMap,
                         app.getStartElapsedTime(),
                         app.getStartUptime());
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 0ce1407..48daef8 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -711,7 +711,7 @@
                 }
             }
             if (profile != null) {
-                long startTime = SystemClock.currentThreadTimeMillis();
+                long startTime = SystemClock.uptimeMillis();
                 // skip background PSS calculation under the following situations:
                 //  - app is capturing camera imagery
                 //  - app is frozen and we have already collected PSS once.
@@ -721,7 +721,7 @@
                         || mService.isCameraActiveForUid(profile.mApp.uid)
                         || mService.mConstants.APP_PROFILER_PSS_PROFILING_DISABLED;
                 long pss = skipPSSCollection ? 0 : Debug.getPss(pid, tmp, null);
-                long endTime = SystemClock.currentThreadTimeMillis();
+                long endTime = SystemClock.uptimeMillis();
                 synchronized (mProfilerLock) {
                     if (pss != 0 && profile.getThread() != null
                             && profile.getSetProcState() == procState
@@ -852,7 +852,7 @@
                 }
             }
             if (profile != null) {
-                long startTime = SystemClock.currentThreadTimeMillis();
+                long startTime = SystemClock.uptimeMillis();
                 // skip background RSS calculation under the following situations:
                 //  - app is capturing camera imagery
                 //  - app is frozen and we have already collected RSS once.
@@ -862,7 +862,7 @@
                         || mService.isCameraActiveForUid(profile.mApp.uid)
                         || mService.mConstants.APP_PROFILER_PSS_PROFILING_DISABLED;
                 long rss = skipRSSCollection ? 0 : Debug.getRss(pid, null);
-                long endTime = SystemClock.currentThreadTimeMillis();
+                long endTime = SystemClock.uptimeMillis();
                 synchronized (mProfilerLock) {
                     if (rss != 0 && profile.getThread() != null
                             && profile.getSetProcState() == procState
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index cd6964e..7f6d62c 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1051,7 +1051,7 @@
 
         assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP());
 
-        postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime);
+        postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime, true);
 
         if (startProfiling) {
             mService.mOomAdjProfiler.oomAdjEnded();
@@ -1073,12 +1073,12 @@
 
     @GuardedBy({"mService", "mProcLock"})
     protected void postUpdateOomAdjInnerLSP(@OomAdjReason int oomAdjReason, ActiveUids activeUids,
-            long now, long nowElapsed, long oldTime) {
+            long now, long nowElapsed, long oldTime, boolean doingAll) {
         mNumNonCachedProcs = 0;
         mNumCachedHiddenProcs = 0;
 
         final boolean allChanged = updateAndTrimProcessLSP(now, nowElapsed, oldTime, activeUids,
-                oomAdjReason);
+                oomAdjReason, doingAll);
         mNumServiceProcs = mNewNumServiceProcs;
 
         if (mService.mAlwaysFinishActivities) {
@@ -1288,7 +1288,8 @@
 
     @GuardedBy({"mService", "mProcLock"})
     private boolean updateAndTrimProcessLSP(final long now, final long nowElapsed,
-            final long oldTime, final ActiveUids activeUids, @OomAdjReason int oomAdjReason) {
+            final long oldTime, final ActiveUids activeUids, @OomAdjReason int oomAdjReason,
+            boolean doingAll) {
         ArrayList<ProcessRecord> lruList = mProcessList.getLruProcessesLOSP();
         final int numLru = lruList.size();
 
@@ -1321,7 +1322,7 @@
             if (!app.isKilledByAm() && app.getThread() != null) {
                 // We don't need to apply the update for the process which didn't get computed
                 if (state.getCompletedAdjSeq() == mAdjSeq) {
-                    applyOomAdjLSP(app, true, now, nowElapsed, oomAdjReason);
+                    applyOomAdjLSP(app, doingAll, now, nowElapsed, oomAdjReason);
                 }
 
                 if (app.isPendingFinishAttach()) {
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index dd75bc0..46bdfe8 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -820,7 +820,7 @@
         computeConnectionsLSP();
 
         assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP());
-        postUpdateOomAdjInnerLSP(oomAdjReason, mActiveUids, now, nowElapsed, oldTime);
+        postUpdateOomAdjInnerLSP(oomAdjReason, mActiveUids, now, nowElapsed, oldTime, true);
     }
 
     /**
@@ -908,7 +908,7 @@
             }
         }
 
-        postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime);
+        postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime, false);
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 27d6c60..48a9d6a 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2088,8 +2088,10 @@
                     + " with non-zero pid:" + app.getPid());
         }
         app.setDisabledCompatChanges(null);
+        app.setLoggableCompatChanges(null);
         if (mPlatformCompat != null) {
             app.setDisabledCompatChanges(mPlatformCompat.getDisabledChanges(app.info));
+            app.setLoggableCompatChanges(mPlatformCompat.getLoggableChanges(app.info));
         }
         final long startSeq = ++mProcStartSeqCounter;
         app.setStartSeq(startSeq);
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 9fa3a8b..b939089 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -259,6 +259,12 @@
     private long[] mDisabledCompatChanges;
 
     /**
+     * Set of compat changes for the process that are intended to be logged to logcat.
+     */
+    @GuardedBy("mService")
+    private long[] mLoggableCompatChanges;
+
+    /**
      * Who is watching for the death.
      */
     @GuardedBy("mService")
@@ -935,11 +941,21 @@
     }
 
     @GuardedBy("mService")
+    long[] getLoggableCompatChanges() {
+        return mLoggableCompatChanges;
+    }
+
+    @GuardedBy("mService")
     void setDisabledCompatChanges(long[] disabledCompatChanges) {
         mDisabledCompatChanges = disabledCompatChanges;
     }
 
     @GuardedBy("mService")
+    void setLoggableCompatChanges(long[] loggableCompatChanges) {
+        mLoggableCompatChanges = loggableCompatChanges;
+    }
+
+    @GuardedBy("mService")
     void unlinkDeathRecipient() {
         if (mDeathRecipient != null && mThread != null) {
             mThread.asBinder().unlinkToDeath(mDeathRecipient, 0);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index d1bda79..7df5fdd 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -170,6 +170,7 @@
         "pixel_connectivity_gps",
         "pixel_system_sw_video",
         "pixel_watch",
+        "platform_compat",
         "platform_security",
         "pmw",
         "power",
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
index 03acf72..d93ff9d 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
@@ -82,13 +82,6 @@
                     Slogf.w(TAG, "No module %s with id %d (HAL AIDL)", name, moduleId);
                     return;
                 }
-                try {
-                    radioModule.setInternalHalCallback();
-                } catch (RemoteException ex) {
-                    Slogf.wtf(TAG, ex, "Broadcast radio module %s with id %d (HAL AIDL) "
-                            + "cannot register HAL callback", name, moduleId);
-                    return;
-                }
                 if (DEBUG) {
                     Slogf.d(TAG, "Loaded broadcast radio module %s with id %d (HAL AIDL)",
                             name, moduleId);
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
index 4b3444d..cd86510 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -246,10 +246,6 @@
         return mProperties;
     }
 
-    void setInternalHalCallback() throws RemoteException {
-        mService.setTunerCallback(mHalTunerCallback);
-    }
-
     TunerSession openSession(android.hardware.radio.ITunerCallback userCb)
             throws RemoteException {
         mLogger.logRadioEvent("Open TunerSession");
@@ -257,10 +253,14 @@
         Boolean antennaConnected;
         RadioManager.ProgramInfo currentProgramInfo;
         synchronized (mLock) {
+            boolean isFirstTunerSession = mAidlTunerSessions.isEmpty();
             tunerSession = new TunerSession(this, mService, userCb);
             mAidlTunerSessions.add(tunerSession);
             antennaConnected = mAntennaConnected;
             currentProgramInfo = mCurrentProgramInfo;
+            if (isFirstTunerSession) {
+                mService.setTunerCallback(mHalTunerCallback);
+            }
         }
         // Propagate state to new client.
         // Note: These callbacks are invoked while holding mLock to prevent race conditions
@@ -284,7 +284,6 @@
         synchronized (mLock) {
             tunerSessions = new TunerSession[mAidlTunerSessions.size()];
             mAidlTunerSessions.toArray(tunerSessions);
-            mAidlTunerSessions.clear();
         }
 
         for (TunerSession tunerSession : tunerSessions) {
@@ -402,6 +401,14 @@
             mAidlTunerSessions.remove(tunerSession);
         }
         onTunerSessionProgramListFilterChanged(null);
+        if (mAidlTunerSessions.isEmpty()) {
+            try {
+                mService.unsetTunerCallback();
+            } catch (RemoteException ex) {
+                Slogf.wtf(TAG, ex, "Failed to unregister HAL callback for module %d",
+                        mProperties.getId());
+            }
+        }
     }
 
     // add to mHandler queue
diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java
index 9102cfd..79025d0 100644
--- a/services/core/java/com/android/server/compat/CompatConfig.java
+++ b/services/core/java/com/android/server/compat/CompatConfig.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.os.Build;
 import android.os.Environment;
 import android.text.TextUtils;
 import android.util.LongArray;
@@ -72,7 +73,6 @@
  * been configured.
  */
 final class CompatConfig {
-
     private static final String TAG = "CompatConfig";
     private static final String APP_COMPAT_DATA_DIR = "/data/misc/appcompat";
     private static final String STATIC_OVERRIDES_PRODUCT_DIR = "/product/etc/appcompat";
@@ -149,6 +149,56 @@
     }
 
     /**
+     * Retrieves the set of changes that are intended to be logged. This includes changes that
+     * target the most recent SDK version and are not disabled.
+     *
+     * @param app the app in question
+     * @return a sorted long array of change IDs
+     */
+    long[] getLoggableChanges(ApplicationInfo app) {
+        LongArray loggable = new LongArray(mChanges.size());
+        for (CompatChange c : mChanges.values()) {
+            long changeId = c.getId();
+            boolean isLatestSdk = isChangeTargetingLatestSdk(c, app.targetSdkVersion);
+            if (c.isEnabled(app, mAndroidBuildClassifier) && isLatestSdk) {
+                loggable.add(changeId);
+            }
+        }
+        final long[] sortedChanges = loggable.toArray();
+        Arrays.sort(sortedChanges);
+        return sortedChanges;
+    }
+
+    /**
+     * Whether the change indicated by the given changeId is targeting the latest SDK version.
+     * @param c             the change for which to check the target SDK version
+     * @param appSdkVersion the target sdk version of the app
+     * @return true if the changeId targets the current sdk version or the current development
+     * version.
+     */
+    boolean isChangeTargetingLatestSdk(CompatChange c, int appSdkVersion) {
+        int maxTargetSdk = maxTargetSdkForCompatChange(c) + 1;
+        if (maxTargetSdk <= 0) {
+            // No max target sdk found.
+            return false;
+        }
+
+        return maxTargetSdk == Build.VERSION_CODES.CUR_DEVELOPMENT || maxTargetSdk == appSdkVersion;
+    }
+
+    /**
+     * Retrieves the CompatChange associated with the given changeId. Will return null if the
+     * changeId is not found. Used only for performance improvement purposes, in order to reduce
+     * lookups.
+     *
+     * @param changeId for which to look up the CompatChange
+     * @return the found compat change, or null if not found.
+     */
+    CompatChange getCompatChange(long changeId) {
+        return mChanges.get(changeId);
+    }
+
+    /**
      * Looks up a change ID by name.
      *
      * @param name name of the change to look up
@@ -164,7 +214,7 @@
     }
 
     /**
-     * Checks if a given change is enabled for a given application.
+     * Checks if a given change id is enabled for a given application.
      *
      * @param changeId the ID of the change in question
      * @param app      app to check for
@@ -173,6 +223,18 @@
      */
     boolean isChangeEnabled(long changeId, ApplicationInfo app) {
         CompatChange c = mChanges.get(changeId);
+        return isChangeEnabled(c, app);
+    }
+
+    /**
+     * Checks if a given change is enabled for a given application.
+     *
+     * @param c   the CompatChange in question
+     * @param app the app to check for
+     * @return {@code true} if the change is enabled for this app. Also returns {@code true} if the
+     * change ID is not known, as unknown changes are enabled by default.
+     */
+    boolean isChangeEnabled(CompatChange c, ApplicationInfo app) {
         if (c == null) {
             // we know nothing about this change: default behaviour is enabled.
             return true;
@@ -301,9 +363,21 @@
     /**
      * Returns the maximum SDK version for which this change can be opted in (or -1 if it is not
      * target SDK gated).
+     *
+     * @param changeId the id of the CompatChange to check for the max target sdk
      */
     int maxTargetSdkForChangeIdOptIn(long changeId) {
         CompatChange c = mChanges.get(changeId);
+        return maxTargetSdkForCompatChange(c);
+    }
+
+    /**
+     * Returns the maximum SDK version for which this change can be opted in (or -1 if it is not
+     * target SDK gated).
+     *
+     * @param c the CompatChange to check for the max target sdk
+     */
+    int maxTargetSdkForCompatChange(CompatChange c) {
         if (c != null && c.getEnableSinceTargetSdk() != -1) {
             return c.getEnableSinceTargetSdk() - 1;
         }
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 6cca130..f8fd0a0 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -120,8 +120,16 @@
         reportChangeInternal(changeId, uid, ChangeReporter.STATE_LOGGED);
     }
 
+    /**
+     * Report the change, but skip over the sdk target version check. This can be used to force the
+     * debug logs.
+     *
+     * @param changeId        of the change to report
+     * @param uid             of the user
+     * @param state           of the change - enabled/disabled/logged
+     */
     private void reportChangeInternal(long changeId, int uid, int state) {
-        mChangeReporter.reportChange(uid, changeId, state);
+        mChangeReporter.reportChange(uid, changeId, state, true);
     }
 
     @Override
@@ -164,15 +172,25 @@
     }
 
     /**
-     * Internal version of {@link #isChangeEnabled(long, ApplicationInfo)}.
+     * Internal version of {@link #isChangeEnabled(long, ApplicationInfo)}. If the provided appInfo
+     * is not null, also reports the change.
+     *
+     * @param changeId of the change to report
+     * @param appInfo  the app to check
      *
      * <p>Does not perform costly permission check.
      */
     public boolean isChangeEnabledInternal(long changeId, ApplicationInfo appInfo) {
-        boolean enabled = isChangeEnabledInternalNoLogging(changeId, appInfo);
+        // Fetch the CompatChange. This is done here instead of in mCompatConfig to avoid multiple
+        // fetches.
+        CompatChange c = mCompatConfig.getCompatChange(changeId);
+
+        boolean enabled = mCompatConfig.isChangeEnabled(c, appInfo);
+        int state = enabled ? ChangeReporter.STATE_ENABLED : ChangeReporter.STATE_DISABLED;
         if (appInfo != null) {
-            reportChangeInternal(changeId, appInfo.uid,
-                    enabled ? ChangeReporter.STATE_ENABLED : ChangeReporter.STATE_DISABLED);
+            boolean isTargetingLatestSdk =
+                    mCompatConfig.isChangeTargetingLatestSdk(c, appInfo.targetSdkVersion);
+            mChangeReporter.reportChange(appInfo.uid, changeId, state, isTargetingLatestSdk);
         }
         return enabled;
     }
@@ -399,6 +417,19 @@
     }
 
     /**
+     * Retrieves the set of changes that should be logged for a given app. Any change ID not in the
+     * returned array is ignored for logging purposes.
+     *
+     * @param appInfo The app in question
+     * @return A sorted long array of change IDs. We use a primitive array to minimize memory
+     * footprint: Every app process will store this array statically so we aim to reduce
+     * overhead as much as possible.
+     */
+    public long[] getLoggableChanges(ApplicationInfo appInfo) {
+        return mCompatConfig.getLoggableChanges(appInfo);
+    }
+
+    /**
      * Look up a change ID by name.
      *
      * @param name Name of the change to look up
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index 3b05b47..a7748f4 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -44,6 +44,15 @@
  * </p>
  */
 abstract class DisplayDevice {
+    /**
+     * Maximum acceptable anisotropy for the output image.
+     *
+     * Necessary to avoid unnecessary scaling when pixels are almost square, as they are non ideal
+     * anyway. For external displays, we expect an anisotropy of about 2% even if the pixels
+     * are, in fact, square due to the imprecision of the display's actual size (parsed from edid
+     * and rounded to the nearest cm).
+     */
+    static final float MAX_ANISOTROPY = 1.025f;
     private static final String TAG = "DisplayDevice";
     private static final Display.Mode EMPTY_DISPLAY_MODE = new Display.Mode.Builder().build();
 
@@ -69,13 +78,21 @@
     // Do not use for any other purpose.
     DisplayDeviceInfo mDebugLastLoggedDeviceInfo;
 
-    public DisplayDevice(DisplayAdapter displayAdapter, IBinder displayToken, String uniqueId,
+    private final boolean mIsAnisotropyCorrectionEnabled;
+
+    DisplayDevice(DisplayAdapter displayAdapter, IBinder displayToken, String uniqueId,
             Context context) {
+        this(displayAdapter, displayToken, uniqueId, context, false);
+    }
+
+    DisplayDevice(DisplayAdapter displayAdapter, IBinder displayToken, String uniqueId,
+            Context context, boolean isAnisotropyCorrectionEnabled) {
         mDisplayAdapter = displayAdapter;
         mDisplayToken = displayToken;
         mUniqueId = uniqueId;
         mDisplayDeviceConfig = null;
         mContext = context;
+        mIsAnisotropyCorrectionEnabled = isAnisotropyCorrectionEnabled;
     }
 
     /**
@@ -143,8 +160,17 @@
         DisplayDeviceInfo displayDeviceInfo = getDisplayDeviceInfoLocked();
         final boolean isRotated = mCurrentOrientation == ROTATION_90
                 || mCurrentOrientation == ROTATION_270;
-        return isRotated ? new Point(displayDeviceInfo.height, displayDeviceInfo.width)
-                : new Point(displayDeviceInfo.width, displayDeviceInfo.height);
+        var width = displayDeviceInfo.width;
+        var height = displayDeviceInfo.height;
+        if (mIsAnisotropyCorrectionEnabled && displayDeviceInfo.yDpi > 0
+                    && displayDeviceInfo.xDpi > 0) {
+            if (displayDeviceInfo.xDpi > displayDeviceInfo.yDpi * MAX_ANISOTROPY) {
+                height = (int) (height * displayDeviceInfo.xDpi / displayDeviceInfo.yDpi + 0.5);
+            } else if (displayDeviceInfo.xDpi * MAX_ANISOTROPY < displayDeviceInfo.yDpi) {
+                width = (int) (width * displayDeviceInfo.yDpi / displayDeviceInfo.xDpi  + 0.5);
+            }
+        }
+        return isRotated ? new Point(height, width) : new Point(width, height);
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 88c24e0..b2fd9ed 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -257,7 +257,8 @@
                 SurfaceControl.DynamicDisplayInfo dynamicInfo,
                 SurfaceControl.DesiredDisplayModeSpecs modeSpecs, boolean isFirstDisplay) {
             super(LocalDisplayAdapter.this, displayToken, UNIQUE_ID_PREFIX + physicalDisplayId,
-                    getContext());
+                    getContext(),
+                    getFeatureFlags().isPixelAnisotropyCorrectionInLogicalDisplayEnabled());
             mPhysicalDisplayId = physicalDisplayId;
             mIsFirstDisplay = isFirstDisplay;
             updateDisplayPropertiesLocked(staticDisplayInfo, dynamicInfo, modeSpecs);
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index db636d6..5eaaf35 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -35,7 +35,6 @@
 
 import com.android.server.display.layout.Layout;
 import com.android.server.display.mode.DisplayModeDirector;
-import com.android.server.wm.utils.DisplayInfoOverrides;
 import com.android.server.wm.utils.InsetUtils;
 
 import java.io.PrintWriter;
@@ -204,7 +203,28 @@
     private SparseArray<SurfaceControl.RefreshRateRange> mThermalRefreshRateThrottling =
             new SparseArray<>();
 
+    /**
+     * If the aspect ratio of the resolution of the display does not match the physical aspect
+     * ratio of the display, then without this feature enabled, picture would appear stretched to
+     * the user. This is because applications assume that they are rendered on square pixels
+     * (meaning density of pixels in x and y directions are equal). This would result into circles
+     * appearing as ellipses to the user.
+     * To compensate for non-square (anisotropic) pixels, if this feature is enabled:
+     * 1. LogicalDisplay will add more pixels for the applications to render on, as if the pixels
+     * were square and occupied the full display.
+     * 2. SurfaceFlinger will squeeze this taller/wider surface into the available number of
+     * physical pixels in the current display resolution.
+     * 3. If a setting on the display itself is set to "fill the entire display panel" then the
+     * display will stretch the pixels to fill the display fully.
+     */
+    private final boolean mIsAnisotropyCorrectionEnabled;
+
     LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) {
+        this(displayId, layerStack, primaryDisplayDevice, false);
+    }
+
+    LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice,
+            boolean isAnisotropyCorrectionEnabled) {
         mDisplayId = displayId;
         mLayerStack = layerStack;
         mPrimaryDisplayDevice = primaryDisplayDevice;
@@ -215,6 +235,7 @@
         mThermalBrightnessThrottlingDataId = DisplayDeviceConfig.DEFAULT_ID;
         mPowerThrottlingDataId = DisplayDeviceConfig.DEFAULT_ID;
         mBaseDisplayInfo.thermalBrightnessThrottlingDataId = mThermalBrightnessThrottlingDataId;
+        mIsAnisotropyCorrectionEnabled = isAnisotropyCorrectionEnabled;
     }
 
     public void setDevicePositionLocked(int position) {
@@ -453,6 +474,14 @@
             int maskedWidth = deviceInfo.width - maskingInsets.left - maskingInsets.right;
             int maskedHeight = deviceInfo.height - maskingInsets.top - maskingInsets.bottom;
 
+            if (mIsAnisotropyCorrectionEnabled && deviceInfo.xDpi > 0 && deviceInfo.yDpi > 0) {
+                if (deviceInfo.xDpi > deviceInfo.yDpi * DisplayDevice.MAX_ANISOTROPY) {
+                    maskedHeight = (int) (maskedHeight * deviceInfo.xDpi / deviceInfo.yDpi + 0.5);
+                } else if (deviceInfo.xDpi * DisplayDevice.MAX_ANISOTROPY < deviceInfo.yDpi) {
+                    maskedWidth = (int) (maskedWidth * deviceInfo.yDpi / deviceInfo.xDpi + 0.5);
+                }
+            }
+
             mBaseDisplayInfo.type = deviceInfo.type;
             mBaseDisplayInfo.address = deviceInfo.address;
             mBaseDisplayInfo.deviceProductInfo = deviceInfo.deviceProductInfo;
@@ -666,6 +695,31 @@
         physWidth -= maskingInsets.left + maskingInsets.right;
         physHeight -= maskingInsets.top + maskingInsets.bottom;
 
+        var displayLogicalWidth = displayInfo.logicalWidth;
+        var displayLogicalHeight = displayInfo.logicalHeight;
+
+        if (mIsAnisotropyCorrectionEnabled && displayDeviceInfo.xDpi > 0
+                    && displayDeviceInfo.yDpi > 0) {
+            if (displayDeviceInfo.xDpi > displayDeviceInfo.yDpi * DisplayDevice.MAX_ANISOTROPY) {
+                var scalingFactor = displayDeviceInfo.yDpi / displayDeviceInfo.xDpi;
+                if (rotated) {
+                    displayLogicalWidth = (int) ((float) displayLogicalWidth * scalingFactor + 0.5);
+                } else {
+                    displayLogicalHeight = (int) ((float) displayLogicalHeight * scalingFactor
+                                                          + 0.5);
+                }
+            } else if (displayDeviceInfo.xDpi * DisplayDevice.MAX_ANISOTROPY
+                               < displayDeviceInfo.yDpi) {
+                var scalingFactor = displayDeviceInfo.xDpi / displayDeviceInfo.yDpi;
+                if (rotated) {
+                    displayLogicalHeight = (int) ((float) displayLogicalHeight * scalingFactor
+                                                          + 0.5);
+                } else {
+                    displayLogicalWidth = (int) ((float) displayLogicalWidth * scalingFactor + 0.5);
+                }
+            }
+        }
+
         // Determine whether the width or height is more constrained to be scaled.
         //    physWidth / displayInfo.logicalWidth    => letter box
         // or physHeight / displayInfo.logicalHeight  => pillar box
@@ -675,16 +729,16 @@
         // comparing them.
         int displayRectWidth, displayRectHeight;
         if ((displayInfo.flags & Display.FLAG_SCALING_DISABLED) != 0 || mDisplayScalingDisabled) {
-            displayRectWidth = displayInfo.logicalWidth;
-            displayRectHeight = displayInfo.logicalHeight;
-        } else if (physWidth * displayInfo.logicalHeight
-                < physHeight * displayInfo.logicalWidth) {
+            displayRectWidth = displayLogicalWidth;
+            displayRectHeight = displayLogicalHeight;
+        } else if (physWidth * displayLogicalHeight
+                < physHeight * displayLogicalWidth) {
             // Letter box.
             displayRectWidth = physWidth;
-            displayRectHeight = displayInfo.logicalHeight * physWidth / displayInfo.logicalWidth;
+            displayRectHeight = displayLogicalHeight * physWidth / displayLogicalWidth;
         } else {
             // Pillar box.
-            displayRectWidth = displayInfo.logicalWidth * physHeight / displayInfo.logicalHeight;
+            displayRectWidth = displayLogicalWidth * physHeight / displayLogicalHeight;
             displayRectHeight = physHeight;
         }
         int displayRectTop = (physHeight - displayRectHeight) / 2;
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 3452e0f..e092fda 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -1151,7 +1151,8 @@
      */
     private LogicalDisplay createNewLogicalDisplayLocked(DisplayDevice device, int displayId) {
         final int layerStack = assignLayerStackLocked(displayId);
-        final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device);
+        final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device,
+                mFlags.isPixelAnisotropyCorrectionInLogicalDisplayEnabled());
         display.updateLocked(mDisplayDeviceRepo);
 
         final DisplayInfo info = display.getDisplayInfoLocked();
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 3c98ee4..15ee937 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -121,6 +121,11 @@
             Flags::refreshRateVotingTelemetry
     );
 
+    private final FlagState mPixelAnisotropyCorrectionEnabled = new FlagState(
+            Flags.FLAG_ENABLE_PIXEL_ANISOTROPY_CORRECTION,
+            Flags::enablePixelAnisotropyCorrection
+    );
+
     private final FlagState mSensorBasedBrightnessThrottling = new FlagState(
             Flags.FLAG_SENSOR_BASED_BRIGHTNESS_THROTTLING,
             Flags::sensorBasedBrightnessThrottling
@@ -259,6 +264,10 @@
         return mRefreshRateVotingTelemetry.isEnabled();
     }
 
+    public boolean isPixelAnisotropyCorrectionInLogicalDisplayEnabled() {
+        return mPixelAnisotropyCorrectionEnabled.isEnabled();
+    }
+
     public boolean isSensorBasedBrightnessThrottlingEnabled() {
         return mSensorBasedBrightnessThrottling.isEnabled();
     }
@@ -290,6 +299,7 @@
         pw.println(" " + mAutoBrightnessModesFlagState);
         pw.println(" " + mFastHdrTransitions);
         pw.println(" " + mRefreshRateVotingTelemetry);
+        pw.println(" " + mPixelAnisotropyCorrectionEnabled);
         pw.println(" " + mSensorBasedBrightnessThrottling);
         pw.println(" " + mRefactorDisplayPowerController);
     }
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 3404527..9bf36e4 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -186,6 +186,14 @@
 }
 
 flag {
+    name: "enable_pixel_anisotropy_correction"
+    namespace: "display_manager"
+    description: "Feature flag for enabling display anisotropy correction through LogicalDisplay upscaling"
+    bug: "317363416"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "sensor_based_brightness_throttling"
     namespace: "display_manager"
     description: "Feature flag for enabling brightness throttling using sensor from config."
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 8b4e1ff..64cbd54 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -1015,12 +1015,15 @@
                 // Infinity means that we want the highest possible refresh rate
                 minRefreshRate = highestRefreshRate;
 
-                if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled) {
-                    // The flag had been turned off, we need to restore the original value
+                if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled
+                        && displayId == Display.DEFAULT_DISPLAY) {
+                    // The flag has been turned off, we need to restore the original value. We'll
+                    // use the peak refresh rate of the default display.
                     Settings.System.putFloatForUser(cr, Settings.System.MIN_REFRESH_RATE,
                             highestRefreshRate, cr.getUserId());
                 }
             } else if (mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled
+                    && displayId == Display.DEFAULT_DISPLAY
                     && Math.round(minRefreshRate) == Math.round(highestRefreshRate)) {
                 // The flag has been turned on, we need to upgrade the setting
                 Settings.System.putFloatForUser(cr, Settings.System.MIN_REFRESH_RATE,
@@ -1033,12 +1036,15 @@
                 // Infinity means that we want the highest possible refresh rate
                 peakRefreshRate = highestRefreshRate;
 
-                if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled) {
-                    // The flag had been turned off, we need to restore the original value
+                if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled
+                        && displayId == Display.DEFAULT_DISPLAY) {
+                    // The flag has been turned off, we need to restore the original value. We'll
+                    // use the peak refresh rate of the default display.
                     Settings.System.putFloatForUser(cr, Settings.System.PEAK_REFRESH_RATE,
                             highestRefreshRate, cr.getUserId());
                 }
             } else if (mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled
+                    && displayId == Display.DEFAULT_DISPLAY
                     && Math.round(peakRefreshRate) == Math.round(highestRefreshRate)) {
                 // The flag has been turned on, we need to upgrade the setting
                 Settings.System.putFloatForUser(cr, Settings.System.PEAK_REFRESH_RATE,
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
index 0ef23e9..e6bf2c9 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
@@ -61,7 +61,6 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 
 /**
@@ -133,7 +132,11 @@
 
         @Override
         public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) {
-            return canGetSystemGrammaticalGender(attributionSource)
+            if (!checkSystemGrammaticalGenderPermission(mPermissionManager, attributionSource)) {
+                throw new SecurityException("AttributionSource: " + attributionSource
+                        + " does not have READ_SYSTEM_GRAMMATICAL_GENDER permission.");
+            }
+            return checkSystemTermsOfAddressIsEnabled()
                     ? GrammaticalInflectionService.this.getSystemGrammaticalGender(
                     attributionSource, userId)
                     : GRAMMATICAL_GENDER_NOT_SPECIFIED;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 996477d..fef5661 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -67,7 +67,6 @@
 import android.annotation.Nullable;
 import android.annotation.UiThread;
 import android.annotation.UserIdInt;
-import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -196,21 +195,16 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.security.InvalidParameterException;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Objects;
 import java.util.OptionalInt;
 import java.util.WeakHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
@@ -730,344 +724,9 @@
     private final CopyOnWriteArrayList<InputMethodListListener> mInputMethodListListeners =
             new CopyOnWriteArrayList<>();
 
-    /**
-     * Internal state snapshot when
-     * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called.
-     *
-     * <p>Calling that IPC endpoint basically means that
-     * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called
-     * back in the current IME process shortly, which will also affect what the current IME starts
-     * receiving from {@link InputMethodService#getCurrentInputConnection()}. In other words, this
-     * snapshot will be taken every time when {@link InputMethodManagerService} is initiating a new
-     * logical input session between the client application and the current IME.</p>
-     *
-     * <p>Be careful to not keep strong references to this object forever, which can prevent
-     * {@link StartInputInfo#mImeToken} and {@link StartInputInfo#mTargetWindow} from being GC-ed.
-     * </p>
-     */
-    private static class StartInputInfo {
-        private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
-
-        final int mSequenceNumber;
-        final long mTimestamp;
-        final long mWallTime;
-        @UserIdInt
-        final int mImeUserId;
-        @NonNull
-        final IBinder mImeToken;
-        final int mImeDisplayId;
-        @NonNull
-        final String mImeId;
-        @StartInputReason
-        final int mStartInputReason;
-        final boolean mRestarting;
-        @UserIdInt
-        final int mTargetUserId;
-        final int mTargetDisplayId;
-        @Nullable
-        final IBinder mTargetWindow;
-        @NonNull
-        final EditorInfo mEditorInfo;
-        @SoftInputModeFlags
-        final int mTargetWindowSoftInputMode;
-        final int mClientBindSequenceNumber;
-
-        StartInputInfo(@UserIdInt int imeUserId, @NonNull IBinder imeToken, int imeDisplayId,
-                @NonNull String imeId, @StartInputReason int startInputReason, boolean restarting,
-                @UserIdInt int targetUserId, int targetDisplayId, @Nullable IBinder targetWindow,
-                @NonNull EditorInfo editorInfo, @SoftInputModeFlags int targetWindowSoftInputMode,
-                int clientBindSequenceNumber) {
-            mSequenceNumber = sSequenceNumber.getAndIncrement();
-            mTimestamp = SystemClock.uptimeMillis();
-            mWallTime = System.currentTimeMillis();
-            mImeUserId = imeUserId;
-            mImeToken = imeToken;
-            mImeDisplayId = imeDisplayId;
-            mImeId = imeId;
-            mStartInputReason = startInputReason;
-            mRestarting = restarting;
-            mTargetUserId = targetUserId;
-            mTargetDisplayId = targetDisplayId;
-            mTargetWindow = targetWindow;
-            mEditorInfo = editorInfo;
-            mTargetWindowSoftInputMode = targetWindowSoftInputMode;
-            mClientBindSequenceNumber = clientBindSequenceNumber;
-        }
-    }
-
     @GuardedBy("ImfLock.class")
     private final WeakHashMap<IBinder, IBinder> mImeTargetWindowMap = new WeakHashMap<>();
 
-    @VisibleForTesting
-    static final class SoftInputShowHideHistory {
-        private final Entry[] mEntries = new Entry[16];
-        private int mNextIndex = 0;
-        private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
-
-        static final class Entry {
-            final int mSequenceNumber = sSequenceNumber.getAndIncrement();
-            @Nullable
-            final ClientState mClientState;
-            @SoftInputModeFlags
-            final int mFocusedWindowSoftInputMode;
-            @SoftInputShowHideReason
-            final int mReason;
-            // The timing of handling showCurrentInputLocked() or hideCurrentInputLocked().
-            final long mTimestamp;
-            final long mWallTime;
-            final boolean mInFullscreenMode;
-            @NonNull
-            final String mFocusedWindowName;
-            @Nullable
-            final EditorInfo mEditorInfo;
-            @NonNull
-            final String mRequestWindowName;
-            @Nullable
-            final String mImeControlTargetName;
-            @Nullable
-            final String mImeTargetNameFromWm;
-            @Nullable
-            final String mImeSurfaceParentName;
-
-            Entry(ClientState client, EditorInfo editorInfo,
-                    String focusedWindowName, @SoftInputModeFlags int softInputMode,
-                    @SoftInputShowHideReason int reason,
-                    boolean inFullscreenMode, String requestWindowName,
-                    @Nullable String imeControlTargetName, @Nullable String imeTargetName,
-                    @Nullable String imeSurfaceParentName) {
-                mClientState = client;
-                mEditorInfo = editorInfo;
-                mFocusedWindowName = focusedWindowName;
-                mFocusedWindowSoftInputMode = softInputMode;
-                mReason = reason;
-                mTimestamp = SystemClock.uptimeMillis();
-                mWallTime = System.currentTimeMillis();
-                mInFullscreenMode = inFullscreenMode;
-                mRequestWindowName = requestWindowName;
-                mImeControlTargetName = imeControlTargetName;
-                mImeTargetNameFromWm = imeTargetName;
-                mImeSurfaceParentName = imeSurfaceParentName;
-            }
-        }
-
-        void addEntry(@NonNull Entry entry) {
-            final int index = mNextIndex;
-            mEntries[index] = entry;
-            mNextIndex = (mNextIndex + 1) % mEntries.length;
-        }
-
-        void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
-            final DateTimeFormatter formatter =
-                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
-                            .withZone(ZoneId.systemDefault());
-
-            for (int i = 0; i < mEntries.length; ++i) {
-                final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
-                if (entry == null) {
-                    continue;
-                }
-                pw.print(prefix);
-                pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":");
-
-                pw.print(prefix);
-                pw.println("  time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
-                        + " (timestamp=" + entry.mTimestamp + ")");
-
-                pw.print(prefix);
-                pw.print("  reason=" + InputMethodDebug.softInputDisplayReasonToString(
-                        entry.mReason));
-                pw.println(" inFullscreenMode=" + entry.mInFullscreenMode);
-
-                pw.print(prefix);
-                pw.println("  requestClient=" + entry.mClientState);
-
-                pw.print(prefix);
-                pw.println("  focusedWindowName=" + entry.mFocusedWindowName);
-
-                pw.print(prefix);
-                pw.println("  requestWindowName=" + entry.mRequestWindowName);
-
-                pw.print(prefix);
-                pw.println("  imeControlTargetName=" + entry.mImeControlTargetName);
-
-                pw.print(prefix);
-                pw.println("  imeTargetNameFromWm=" + entry.mImeTargetNameFromWm);
-
-                pw.print(prefix);
-                pw.println("  imeSurfaceParentName=" + entry.mImeSurfaceParentName);
-
-                pw.print(prefix);
-                pw.print("  editorInfo:");
-                if (entry.mEditorInfo != null) {
-                    pw.print(" inputType=" + entry.mEditorInfo.inputType);
-                    pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions);
-                    pw.println(" fieldId (viewId)=" + entry.mEditorInfo.fieldId);
-                } else {
-                    pw.println(" null");
-                }
-
-                pw.print(prefix);
-                pw.println("  focusedWindowSoftInputMode=" + InputMethodDebug.softInputModeToString(
-                        entry.mFocusedWindowSoftInputMode));
-            }
-        }
-    }
-
-    /**
-     * A ring buffer to store the history of {@link StartInputInfo}.
-     */
-    private static final class StartInputHistory {
-        /**
-         * Entry size for non low-RAM devices.
-         *
-         * <p>TODO: Consider to follow what other system services have been doing to manage
-         * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
-         */
-        private static final int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 32;
-
-        /**
-         * Entry size for low-RAM devices.
-         *
-         * <p>TODO: Consider to follow what other system services have been doing to manage
-         * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
-         */
-        private static final int ENTRY_SIZE_FOR_LOW_RAM_DEVICE = 5;
-
-        private static int getEntrySize() {
-            if (ActivityManager.isLowRamDeviceStatic()) {
-                return ENTRY_SIZE_FOR_LOW_RAM_DEVICE;
-            } else {
-                return ENTRY_SIZE_FOR_HIGH_RAM_DEVICE;
-            }
-        }
-
-        /**
-         * Backing store for the ring buffer.
-         */
-        private final Entry[] mEntries = new Entry[getEntrySize()];
-
-        /**
-         * An index of {@link #mEntries}, to which next {@link #addEntry(StartInputInfo)} should
-         * write.
-         */
-        private int mNextIndex = 0;
-
-        /**
-         * Recyclable entry to store the information in {@link StartInputInfo}.
-         */
-        private static final class Entry {
-            int mSequenceNumber;
-            long mTimestamp;
-            long mWallTime;
-            @UserIdInt
-            int mImeUserId;
-            @NonNull
-            String mImeTokenString;
-            int mImeDisplayId;
-            @NonNull
-            String mImeId;
-            @StartInputReason
-            int mStartInputReason;
-            boolean mRestarting;
-            @UserIdInt
-            int mTargetUserId;
-            int mTargetDisplayId;
-            @NonNull
-            String mTargetWindowString;
-            @NonNull
-            EditorInfo mEditorInfo;
-            @SoftInputModeFlags
-            int mTargetWindowSoftInputMode;
-            int mClientBindSequenceNumber;
-
-            Entry(@NonNull StartInputInfo original) {
-                set(original);
-            }
-
-            void set(@NonNull StartInputInfo original) {
-                mSequenceNumber = original.mSequenceNumber;
-                mTimestamp = original.mTimestamp;
-                mWallTime = original.mWallTime;
-                mImeUserId = original.mImeUserId;
-                // Intentionally convert to String so as not to keep a strong reference to a Binder
-                // object.
-                mImeTokenString = String.valueOf(original.mImeToken);
-                mImeDisplayId = original.mImeDisplayId;
-                mImeId = original.mImeId;
-                mStartInputReason = original.mStartInputReason;
-                mRestarting = original.mRestarting;
-                mTargetUserId = original.mTargetUserId;
-                mTargetDisplayId = original.mTargetDisplayId;
-                // Intentionally convert to String so as not to keep a strong reference to a Binder
-                // object.
-                mTargetWindowString = String.valueOf(original.mTargetWindow);
-                mEditorInfo = original.mEditorInfo;
-                mTargetWindowSoftInputMode = original.mTargetWindowSoftInputMode;
-                mClientBindSequenceNumber = original.mClientBindSequenceNumber;
-            }
-        }
-
-        /**
-         * Add a new entry and discard the oldest entry as needed.
-         * @param info {@link StartInputInfo} to be added.
-         */
-        void addEntry(@NonNull StartInputInfo info) {
-            final int index = mNextIndex;
-            if (mEntries[index] == null) {
-                mEntries[index] = new Entry(info);
-            } else {
-                mEntries[index].set(info);
-            }
-            mNextIndex = (mNextIndex + 1) % mEntries.length;
-        }
-
-        void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
-            final DateTimeFormatter formatter =
-                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
-                            .withZone(ZoneId.systemDefault());
-
-            for (int i = 0; i < mEntries.length; ++i) {
-                final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
-                if (entry == null) {
-                    continue;
-                }
-                pw.print(prefix);
-                pw.println("StartInput #" + entry.mSequenceNumber + ":");
-
-                pw.print(prefix);
-                pw.println("  time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
-                        + " (timestamp=" + entry.mTimestamp + ")"
-                        + " reason="
-                        + InputMethodDebug.startInputReasonToString(entry.mStartInputReason)
-                        + " restarting=" + entry.mRestarting);
-
-                pw.print(prefix);
-                pw.print("  imeToken=" + entry.mImeTokenString + " [" + entry.mImeId + "]");
-                pw.print(" imeUserId=" + entry.mImeUserId);
-                pw.println(" imeDisplayId=" + entry.mImeDisplayId);
-
-                pw.print(prefix);
-                pw.println("  targetWin=" + entry.mTargetWindowString
-                        + " [" + entry.mEditorInfo.packageName + "]"
-                        + " targetUserId=" + entry.mTargetUserId
-                        + " targetDisplayId=" + entry.mTargetDisplayId
-                        + " clientBindSeq=" + entry.mClientBindSequenceNumber);
-
-                pw.print(prefix);
-                pw.println("  softInputMode=" + InputMethodDebug.softInputModeToString(
-                        entry.mTargetWindowSoftInputMode));
-
-                pw.print(prefix);
-                pw.println("  inputType=0x" + Integer.toHexString(entry.mEditorInfo.inputType)
-                        + " imeOptions=0x" + Integer.toHexString(entry.mEditorInfo.imeOptions)
-                        + " fieldId=0x" + Integer.toHexString(entry.mEditorInfo.fieldId)
-                        + " fieldName=" + entry.mEditorInfo.fieldName
-                        + " actionId=" + entry.mEditorInfo.actionId
-                        + " actionLabel=" + entry.mEditorInfo.actionLabel);
-            }
-        }
-    }
-
     @GuardedBy("ImfLock.class")
     @NonNull
     private final StartInputHistory mStartInputHistory = new StartInputHistory();
diff --git a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
new file mode 100644
index 0000000..3023603
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+
+final class SoftInputShowHideHistory {
+    private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
+
+    private final Entry[] mEntries = new Entry[16];
+    private int mNextIndex = 0;
+
+    static final class Entry {
+        final int mSequenceNumber = sSequenceNumber.getAndIncrement();
+        @Nullable
+        final ClientState mClientState;
+        @WindowManager.LayoutParams.SoftInputModeFlags
+        final int mFocusedWindowSoftInputMode;
+        @SoftInputShowHideReason
+        final int mReason;
+        // The timing of handling showCurrentInputLocked() or hideCurrentInputLocked().
+        final long mTimestamp;
+        final long mWallTime;
+        final boolean mInFullscreenMode;
+        @NonNull
+        final String mFocusedWindowName;
+        @Nullable
+        final EditorInfo mEditorInfo;
+        @NonNull
+        final String mRequestWindowName;
+        @Nullable
+        final String mImeControlTargetName;
+        @Nullable
+        final String mImeTargetNameFromWm;
+        @Nullable
+        final String mImeSurfaceParentName;
+
+        Entry(ClientState client, EditorInfo editorInfo,
+                String focusedWindowName,
+                @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode,
+                @SoftInputShowHideReason int reason,
+                boolean inFullscreenMode, String requestWindowName,
+                @Nullable String imeControlTargetName, @Nullable String imeTargetName,
+                @Nullable String imeSurfaceParentName) {
+            mClientState = client;
+            mEditorInfo = editorInfo;
+            mFocusedWindowName = focusedWindowName;
+            mFocusedWindowSoftInputMode = softInputMode;
+            mReason = reason;
+            mTimestamp = SystemClock.uptimeMillis();
+            mWallTime = System.currentTimeMillis();
+            mInFullscreenMode = inFullscreenMode;
+            mRequestWindowName = requestWindowName;
+            mImeControlTargetName = imeControlTargetName;
+            mImeTargetNameFromWm = imeTargetName;
+            mImeSurfaceParentName = imeSurfaceParentName;
+        }
+    }
+
+    void addEntry(@NonNull Entry entry) {
+        final int index = mNextIndex;
+        mEntries[index] = entry;
+        mNextIndex = (mNextIndex + 1) % mEntries.length;
+    }
+
+    void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+        final DateTimeFormatter formatter =
+                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+                        .withZone(ZoneId.systemDefault());
+
+        for (int i = 0; i < mEntries.length; ++i) {
+            final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
+            if (entry == null) {
+                continue;
+            }
+            pw.print(prefix);
+            pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":");
+
+            pw.print(prefix);
+            pw.println("  time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
+                    + " (timestamp=" + entry.mTimestamp + ")");
+
+            pw.print(prefix);
+            pw.print("  reason=" + InputMethodDebug.softInputDisplayReasonToString(
+                    entry.mReason));
+            pw.println(" inFullscreenMode=" + entry.mInFullscreenMode);
+
+            pw.print(prefix);
+            pw.println("  requestClient=" + entry.mClientState);
+
+            pw.print(prefix);
+            pw.println("  focusedWindowName=" + entry.mFocusedWindowName);
+
+            pw.print(prefix);
+            pw.println("  requestWindowName=" + entry.mRequestWindowName);
+
+            pw.print(prefix);
+            pw.println("  imeControlTargetName=" + entry.mImeControlTargetName);
+
+            pw.print(prefix);
+            pw.println("  imeTargetNameFromWm=" + entry.mImeTargetNameFromWm);
+
+            pw.print(prefix);
+            pw.println("  imeSurfaceParentName=" + entry.mImeSurfaceParentName);
+
+            pw.print(prefix);
+            pw.print("  editorInfo:");
+            if (entry.mEditorInfo != null) {
+                pw.print(" inputType=" + entry.mEditorInfo.inputType);
+                pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions);
+                pw.println(" fieldId (viewId)=" + entry.mEditorInfo.fieldId);
+            } else {
+                pw.println(" null");
+            }
+
+            pw.print(prefix);
+            pw.println("  focusedWindowSoftInputMode=" + InputMethodDebug.softInputModeToString(
+                    entry.mFocusedWindowSoftInputMode));
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/StartInputHistory.java b/services/core/java/com/android/server/inputmethod/StartInputHistory.java
new file mode 100644
index 0000000..3a39434
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/StartInputHistory.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.StartInputReason;
+
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+/**
+ * A ring buffer to store the history of {@link StartInputInfo}.
+ */
+final class StartInputHistory {
+    /**
+     * Entry size for non low-RAM devices.
+     *
+     * <p>TODO: Consider to follow what other system services have been doing to manage
+     * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
+     */
+    private static final int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 32;
+
+    /**
+     * Entry size for low-RAM devices.
+     *
+     * <p>TODO: Consider to follow what other system services have been doing to manage
+     * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
+     */
+    private static final int ENTRY_SIZE_FOR_LOW_RAM_DEVICE = 5;
+
+    private static int getEntrySize() {
+        if (ActivityManager.isLowRamDeviceStatic()) {
+            return ENTRY_SIZE_FOR_LOW_RAM_DEVICE;
+        } else {
+            return ENTRY_SIZE_FOR_HIGH_RAM_DEVICE;
+        }
+    }
+
+    /**
+     * Backing store for the ring buffer.
+     */
+    private final Entry[] mEntries = new Entry[getEntrySize()];
+
+    /**
+     * An index of {@link #mEntries}, to which next
+     * {@link #addEntry(StartInputInfo)} should
+     * write.
+     */
+    private int mNextIndex = 0;
+
+    /**
+     * Recyclable entry to store the information in {@link StartInputInfo}.
+     */
+    private static final class Entry {
+        int mSequenceNumber;
+        long mTimestamp;
+        long mWallTime;
+        @UserIdInt
+        int mImeUserId;
+        @NonNull
+        String mImeTokenString;
+        int mImeDisplayId;
+        @NonNull
+        String mImeId;
+        @StartInputReason
+        int mStartInputReason;
+        boolean mRestarting;
+        @UserIdInt
+        int mTargetUserId;
+        int mTargetDisplayId;
+        @NonNull
+        String mTargetWindowString;
+        @NonNull
+        EditorInfo mEditorInfo;
+        @WindowManager.LayoutParams.SoftInputModeFlags
+        int mTargetWindowSoftInputMode;
+        int mClientBindSequenceNumber;
+
+        Entry(@NonNull StartInputInfo original) {
+            set(original);
+        }
+
+        void set(@NonNull StartInputInfo original) {
+            mSequenceNumber = original.mSequenceNumber;
+            mTimestamp = original.mTimestamp;
+            mWallTime = original.mWallTime;
+            mImeUserId = original.mImeUserId;
+            // Intentionally convert to String so as not to keep a strong reference to a Binder
+            // object.
+            mImeTokenString = String.valueOf(original.mImeToken);
+            mImeDisplayId = original.mImeDisplayId;
+            mImeId = original.mImeId;
+            mStartInputReason = original.mStartInputReason;
+            mRestarting = original.mRestarting;
+            mTargetUserId = original.mTargetUserId;
+            mTargetDisplayId = original.mTargetDisplayId;
+            // Intentionally convert to String so as not to keep a strong reference to a Binder
+            // object.
+            mTargetWindowString = String.valueOf(original.mTargetWindow);
+            mEditorInfo = original.mEditorInfo;
+            mTargetWindowSoftInputMode = original.mTargetWindowSoftInputMode;
+            mClientBindSequenceNumber = original.mClientBindSequenceNumber;
+        }
+    }
+
+    /**
+     * Add a new entry and discard the oldest entry as needed.
+     *
+     * @param info {@link StartInputInfo} to be added.
+     */
+    void addEntry(@NonNull StartInputInfo info) {
+        final int index = mNextIndex;
+        if (mEntries[index] == null) {
+            mEntries[index] = new Entry(info);
+        } else {
+            mEntries[index].set(info);
+        }
+        mNextIndex = (mNextIndex + 1) % mEntries.length;
+    }
+
+    void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+        final DateTimeFormatter formatter =
+                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+                        .withZone(ZoneId.systemDefault());
+
+        for (int i = 0; i < mEntries.length; ++i) {
+            final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
+            if (entry == null) {
+                continue;
+            }
+            pw.print(prefix);
+            pw.println("StartInput #" + entry.mSequenceNumber + ":");
+
+            pw.print(prefix);
+            pw.println("  time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
+                    + " (timestamp=" + entry.mTimestamp + ")"
+                    + " reason="
+                    + InputMethodDebug.startInputReasonToString(entry.mStartInputReason)
+                    + " restarting=" + entry.mRestarting);
+
+            pw.print(prefix);
+            pw.print("  imeToken=" + entry.mImeTokenString + " [" + entry.mImeId + "]");
+            pw.print(" imeUserId=" + entry.mImeUserId);
+            pw.println(" imeDisplayId=" + entry.mImeDisplayId);
+
+            pw.print(prefix);
+            pw.println("  targetWin=" + entry.mTargetWindowString
+                    + " [" + entry.mEditorInfo.packageName + "]"
+                    + " targetUserId=" + entry.mTargetUserId
+                    + " targetDisplayId=" + entry.mTargetDisplayId
+                    + " clientBindSeq=" + entry.mClientBindSequenceNumber);
+
+            pw.print(prefix);
+            pw.println("  softInputMode=" + InputMethodDebug.softInputModeToString(
+                    entry.mTargetWindowSoftInputMode));
+
+            pw.print(prefix);
+            pw.println("  inputType=0x" + Integer.toHexString(entry.mEditorInfo.inputType)
+                    + " imeOptions=0x" + Integer.toHexString(entry.mEditorInfo.imeOptions)
+                    + " fieldId=0x" + Integer.toHexString(entry.mEditorInfo.fieldId)
+                    + " fieldName=" + entry.mEditorInfo.fieldName
+                    + " actionId=" + entry.mEditorInfo.actionId
+                    + " actionLabel=" + entry.mEditorInfo.actionLabel);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/StartInputInfo.java b/services/core/java/com/android/server/inputmethod/StartInputInfo.java
new file mode 100644
index 0000000..1cff737
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/StartInputInfo.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.inputmethodservice.InputMethodService;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import com.android.internal.inputmethod.IInputMethod;
+import com.android.internal.inputmethod.StartInputReason;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Internal state snapshot when
+ * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called.
+ *
+ * <p>Calling that IPC endpoint basically means that
+ * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called
+ * back in the current IME process shortly, which will also affect what the current IME starts
+ * receiving from {@link InputMethodService#getCurrentInputConnection()}. In other words, this
+ * snapshot will be taken every time when {@link InputMethodManagerService} is initiating a new
+ * logical input session between the client application and the current IME.</p>
+ *
+ * <p>Be careful to not keep strong references to this object forever, which can prevent
+ * {@link StartInputInfo#mImeToken} and {@link StartInputInfo#mTargetWindow} from being GC-ed.
+ * </p>
+ */
+final class StartInputInfo {
+    private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
+
+    final int mSequenceNumber;
+    final long mTimestamp;
+    final long mWallTime;
+    @UserIdInt
+    final int mImeUserId;
+    @NonNull
+    final IBinder mImeToken;
+    final int mImeDisplayId;
+    @NonNull
+    final String mImeId;
+    @StartInputReason
+    final int mStartInputReason;
+    final boolean mRestarting;
+    @UserIdInt
+    final int mTargetUserId;
+    final int mTargetDisplayId;
+    @Nullable
+    final IBinder mTargetWindow;
+    @NonNull
+    final EditorInfo mEditorInfo;
+    @WindowManager.LayoutParams.SoftInputModeFlags
+    final int mTargetWindowSoftInputMode;
+    final int mClientBindSequenceNumber;
+
+    StartInputInfo(@UserIdInt int imeUserId, @NonNull IBinder imeToken, int imeDisplayId,
+            @NonNull String imeId, @StartInputReason int startInputReason, boolean restarting,
+            @UserIdInt int targetUserId, int targetDisplayId, @Nullable IBinder targetWindow,
+            @NonNull EditorInfo editorInfo,
+            @WindowManager.LayoutParams.SoftInputModeFlags int targetWindowSoftInputMode,
+            int clientBindSequenceNumber) {
+        mSequenceNumber = sSequenceNumber.getAndIncrement();
+        mTimestamp = SystemClock.uptimeMillis();
+        mWallTime = System.currentTimeMillis();
+        mImeUserId = imeUserId;
+        mImeToken = imeToken;
+        mImeDisplayId = imeDisplayId;
+        mImeId = imeId;
+        mStartInputReason = startInputReason;
+        mRestarting = restarting;
+        mTargetUserId = targetUserId;
+        mTargetDisplayId = targetDisplayId;
+        mTargetWindow = targetWindow;
+        mEditorInfo = editorInfo;
+        mTargetWindowSoftInputMode = targetWindowSoftInputMode;
+        mClientBindSequenceNumber = clientBindSequenceNumber;
+    }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e7455db..c38fbda 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -12054,10 +12054,17 @@
         @Override
         public void onServiceAdded(ManagedServiceInfo info) {
             if (lifetimeExtensionRefactor()) {
-                // We explicitly check the status bar permission for the uid in the info object.
-                // We can't use the calling uid here because it's probably always system server.
-                // Note that this will also be true for the shell.
-                info.isSystemUi = getContext().checkPermission(
+                // Generally, only System or System UI should have the permissions to call
+                // registerSystemService.
+                // isCallerSystemOrPhone tells us whether the caller is System. We negate this,
+                // to eliminate cases where the service was added by the system. This leaves
+                // services registered by system server.
+                // To identify system UI, we explicitly check the status bar permission for the
+                // uid in the info object.
+                // We can't use the calling uid here because it belongs to system server.
+                // Note that this will also return true for the shell, but we deem this
+                // acceptable, for the purposes of testing.
+                info.isSystemUi = !isCallerSystemOrPhone() && getContext().checkPermission(
                         android.Manifest.permission.STATUS_BAR_SERVICE, -1, info.uid)
                         == PERMISSION_GRANTED;
             }
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index c7ebb3c..c6bb99e 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -2116,6 +2116,18 @@
 
         @RequiresPermission(READ_FRAME_BUFFER)
         @Override
+        public void saveViewCaptureData() {
+            int status = checkCallingOrSelfPermissionForPreflight(mContext, READ_FRAME_BUFFER);
+            if (PERMISSION_GRANTED == status) {
+                forEachViewCaptureWindow(this::dumpViewCaptureDataToWmTrace);
+            } else {
+                Log.w(TAG, "caller lacks permissions to save view capture data");
+            }
+        }
+
+
+        @RequiresPermission(READ_FRAME_BUFFER)
+        @Override
         public void registerDumpCallback(@NonNull IDumpCallback cb) {
             int status = checkCallingOrSelfPermissionForPreflight(mContext, READ_FRAME_BUFFER);
             if (PERMISSION_GRANTED == status) {
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index c2f74a8..c9fd261 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -155,7 +155,8 @@
             UserManager.DISALLOW_CONFIG_DEFAULT_APPS,
             UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO,
             UserManager.DISALLOW_SIM_GLOBALLY,
-            UserManager.DISALLOW_ASSIST_CONTENT
+            UserManager.DISALLOW_ASSIST_CONTENT,
+            UserManager.DISALLOW_THREAD_NETWORK
     });
 
     public static final Set<String> DEPRECATED_USER_RESTRICTIONS = Sets.newArraySet(
@@ -206,7 +207,8 @@
             UserManager.DISALLOW_ADD_WIFI_CONFIG,
             UserManager.DISALLOW_CELLULAR_2G,
             UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO,
-            UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO
+            UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO,
+            UserManager.DISALLOW_THREAD_NETWORK
     );
 
     /**
@@ -252,7 +254,8 @@
                     UserManager.DISALLOW_ADD_WIFI_CONFIG,
                     UserManager.DISALLOW_CELLULAR_2G,
                     UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO,
-                    UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO
+                    UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO,
+                    UserManager.DISALLOW_THREAD_NETWORK
             );
 
     /**
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 5974ac8..266418f 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4315,6 +4315,7 @@
             boolean allowDuringSetup) {
         if (allowDuringSetup || isUserSetupComplete()) {
             mContext.startActivityAsUser(intent, bundle, handle);
+            dismissKeyboardShortcutsMenu();
         } else {
             Slog.i(TAG, "Not starting activity because user setup is in progress: " + intent);
         }
@@ -4365,6 +4366,7 @@
         if (statusbar != null) {
             statusbar.showRecentApps(triggeredFromAltTab);
         }
+        dismissKeyboardShortcutsMenu();
     }
 
     private void toggleKeyboardShortcutsMenu(int deviceId) {
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index a914c07..b616d24 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -107,7 +107,9 @@
 
     ContentRecorder(@NonNull DisplayContent displayContent) {
         this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId),
-                new DisplayManagerFlags().isConnectedDisplayManagementEnabled());
+                new DisplayManagerFlags().isConnectedDisplayManagementEnabled()
+                        && !new DisplayManagerFlags()
+                                    .isPixelAnisotropyCorrectionInLogicalDisplayEnabled());
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 877378c..a29cb60 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -173,18 +173,25 @@
                     mDisplayContent.mInitialDisplayHeight);
             final int fromRotation = mDisplayContent.getRotation();
 
-            onStartCollect.run();
+            mDisplayContent.mAtmService.deferWindowLayout();
+            try {
+                onStartCollect.run();
 
-            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
-                    "DeferredDisplayUpdater: applied DisplayInfo after deferring");
+                ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
+                        "DeferredDisplayUpdater: applied DisplayInfo after deferring");
 
-            if (physicalDisplayUpdated) {
-                onDisplayUpdated(transition, fromRotation, startBounds);
-            } else {
-                final TransitionRequestInfo.DisplayChange displayChange =
-                        getCurrentDisplayChange(fromRotation, startBounds);
-                mDisplayContent.mTransitionController.requestStartTransition(transition,
-                        /* startTask= */ null, /* remoteTransition= */ null, displayChange);
+                if (physicalDisplayUpdated) {
+                    onDisplayUpdated(transition, fromRotation, startBounds);
+                } else {
+                    final TransitionRequestInfo.DisplayChange displayChange =
+                            getCurrentDisplayChange(fromRotation, startBounds);
+                    mDisplayContent.mTransitionController.requestStartTransition(transition,
+                            /* startTask= */ null, /* remoteTransition= */ null, displayChange);
+                }
+            } finally {
+                // Run surface placement after requestStartTransition, so shell side can receive
+                // the transition request before handling task info changes.
+                mDisplayContent.mAtmService.continueWindowLayout();
             }
         });
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 282ecc7..837d08b 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -6210,7 +6210,12 @@
      * @param onDisplayChangeApplied callback that is called when the changes are applied
      */
     void requestDisplayUpdate(@NonNull Runnable onDisplayChangeApplied) {
-        mDisplayUpdater.updateDisplayInfo(onDisplayChangeApplied);
+        mAtmService.deferWindowLayout();
+        try {
+            mDisplayUpdater.updateDisplayInfo(onDisplayChangeApplied);
+        } finally {
+            mAtmService.continueWindowLayout();
+        }
     }
 
     void onDisplayInfoUpdated(@NonNull DisplayInfo newDisplayInfo) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index c37946b..ee72db0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -3395,7 +3395,9 @@
                     if (shouldMigrateV1ToDevicePolicyEngine()) {
                         migrateV1PoliciesToDevicePolicyEngine();
                     }
+                    maybeMigratePoliciesPostUpgradeToDevicePolicyEngineLocked();
                     migratePoliciesToPolicyEngineLocked();
+
                 }
                 maybeStartSecurityLogMonitorOnActivityManagerReady();
                 break;
@@ -16877,6 +16879,8 @@
     private int checkDeviceOwnerProvisioningPreCondition(@UserIdInt int callingUserId) {
         synchronized (getLockObject()) {
             final int deviceOwnerUserId = mInjector.userManagerIsHeadlessSystemUserMode()
+                    && (!Flags.headlessDeviceOwnerProvisioningFixEnabled()
+                    || getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED)
                     ? UserHandle.USER_SYSTEM
                     : callingUserId;
             Slogf.i(LOG_TAG, "Calling user %d, device owner will be set on user %d",
@@ -21549,10 +21553,21 @@
             setTimeAndTimezone(provisioningParams.getTimeZone(), provisioningParams.getLocalTime());
             setLocale(provisioningParams.getLocale());
 
+
+
+            boolean isSingleUserMode;
+            if (Flags.headlessDeviceOwnerProvisioningFixEnabled()) {
+                DeviceAdminInfo adminInfo = findAdmin(
+                        deviceAdmin, caller.getUserId(), /* throwForMissingPermission= */ false);
+                isSingleUserMode = (adminInfo != null && adminInfo.getHeadlessDeviceOwnerMode()
+                        == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER);
+            } else {
+                isSingleUserMode =
+                        (getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER);
+            }
             int deviceOwnerUserId = Flags.headlessDeviceOwnerSingleUserEnabled()
-                    && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER
-                    ? mUserManagerInternal.getMainUserId()
-                    : UserHandle.USER_SYSTEM;
+                    && isSingleUserMode
+                    ? mUserManagerInternal.getMainUserId() : UserHandle.USER_SYSTEM;
 
             if (!removeNonRequiredAppsForManagedDevice(
                     deviceOwnerUserId,
@@ -23736,7 +23751,9 @@
             if (!canForceMigration && !shouldMigrateV1ToDevicePolicyEngine()) {
                 return false;
             }
-            return migrateV1PoliciesToDevicePolicyEngine();
+            boolean migrated = migrateV1PoliciesToDevicePolicyEngine();
+            migrated &= migratePoliciesPostUpgradeToDevicePolicyEngineLocked();
+            return migrated;
         });
     }
 
@@ -23765,6 +23782,30 @@
 
     /**
      * Migrates the initial set of policies to use policy engine.
+     * [b/318497672] Migrate policies that weren't migrated properly in the initial migration on
+     * update from Android T to Android U
+     */
+    private void maybeMigratePoliciesPostUpgradeToDevicePolicyEngineLocked() {
+        if (!mOwners.isMigratedToPolicyEngine() || mOwners.isMigratedPostUpdate()) {
+            return;
+        }
+        migratePoliciesPostUpgradeToDevicePolicyEngineLocked();
+        mOwners.markPostUpgradeMigration();
+    }
+
+    private boolean migratePoliciesPostUpgradeToDevicePolicyEngineLocked() {
+        try {
+            migrateScreenCapturePolicyLocked();
+            migrateLockTaskPolicyLocked();
+            return true;
+        } catch (Exception e) {
+            Slogf.e(LOG_TAG, e, "Error occurred during post upgrade migration to the device "
+                    + "policy engine.");
+            return false;
+        }
+    }
+
+    /**
      * @return {@code true} if policies were migrated successfully, {@code false} otherwise.
      */
     private boolean migrateV1PoliciesToDevicePolicyEngine() {
@@ -23777,7 +23818,6 @@
                         migrateAutoTimezonePolicy();
                         migratePermissionGrantStatePolicies();
                     }
-                    migrateScreenCapturePolicyLocked();
                     migratePermittedInputMethodsPolicyLocked();
                     migrateAccountManagementDisabledPolicyLocked();
                     migrateUserControlDisabledPackagesLocked();
@@ -23858,14 +23898,12 @@
 
     private void migrateScreenCapturePolicyLocked() {
         Binder.withCleanCallingIdentity(() -> {
-            if (mPolicyCache.getScreenCaptureDisallowedUser() == UserHandle.USER_NULL) {
-                return;
-            }
             ActiveAdmin admin = getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked();
             if (admin != null
                     && ((isDeviceOwner(admin) && admin.disableScreenCapture)
                     || (admin.getParentActiveAdmin() != null
                     && admin.getParentActiveAdmin().disableScreenCapture))) {
+
                 EnforcingAdmin enforcingAdmin = EnforcingAdmin.createEnterpriseEnforcingAdmin(
                         admin.info.getComponent(),
                         admin.getUserHandle().getIdentifier(),
@@ -23894,6 +23932,48 @@
         });
     }
 
+    private void migrateLockTaskPolicyLocked() {
+        Binder.withCleanCallingIdentity(() -> {
+            ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+            if (deviceOwner != null) {
+                int doUserId = deviceOwner.getUserHandle().getIdentifier();
+                DevicePolicyData policies = getUserData(doUserId);
+                List<String> packages = policies.mLockTaskPackages;
+                int features = policies.mLockTaskFeatures;
+                // TODO: find out about persistent preferred activities
+                if (!packages.isEmpty()) {
+                    setLockTaskPolicyInPolicyEngine(deviceOwner, doUserId, packages, features);
+                }
+            }
+
+            for (int userId : mUserManagerInternal.getUserIds()) {
+                ActiveAdmin profileOwner = getProfileOwnerLocked(userId);
+                if (profileOwner != null && canDPCManagedUserUseLockTaskLocked(userId)) {
+                    DevicePolicyData policies = getUserData(userId);
+                    List<String> packages = policies.mLockTaskPackages;
+                    int features = policies.mLockTaskFeatures;
+                    if (!packages.isEmpty()) {
+                        setLockTaskPolicyInPolicyEngine(profileOwner, userId, packages, features);
+                    }
+                }
+            }
+        });
+    }
+
+    private void setLockTaskPolicyInPolicyEngine(
+            ActiveAdmin admin, int userId, List<String> packages, int features) {
+        EnforcingAdmin enforcingAdmin =
+                EnforcingAdmin.createEnterpriseEnforcingAdmin(
+                        admin.info.getComponent(),
+                        userId,
+                        admin);
+        mDevicePolicyEngine.setLocalPolicy(
+                PolicyDefinition.LOCK_TASK,
+                enforcingAdmin,
+                new LockTaskPolicy(new HashSet<>(packages), features),
+                userId);
+    }
+
     private void migratePermittedInputMethodsPolicyLocked() {
         Binder.withCleanCallingIdentity(() -> {
             List<UserInfo> users = mUserManager.getUsers();
@@ -24256,4 +24336,13 @@
 
         return mDevicePolicyEngine.getMaxPolicyStorageLimit();
     }
+
+    @Override
+    public int getHeadlessDeviceOwnerMode(String callerPackageName) {
+        final CallerIdentity caller = getCallerIdentity(callerPackageName);
+        enforcePermission(MANAGE_PROFILE_AND_DEVICE_OWNERS, caller.getPackageName(),
+                caller.getUserId());
+
+        return Binder.withCleanCallingIdentity(() -> getHeadlessDeviceOwnerMode());
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index c5a9888..7912cbc 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -623,12 +623,25 @@
         }
     }
 
+    void markPostUpgradeMigration() {
+        synchronized (mData) {
+            mData.mPoliciesMigratedPostUpdate = true;
+            mData.writeDeviceOwner();
+        }
+    }
+
     boolean isSecurityLoggingMigrated() {
         synchronized (mData) {
             return mData.mSecurityLoggingMigrated;
         }
     }
 
+    boolean isMigratedPostUpdate() {
+        synchronized (mData) {
+            return mData.mPoliciesMigratedPostUpdate;
+        }
+    }
+
     @GuardedBy("mData")
     void pushToAppOpsLocked() {
         if (!mSystemReady) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
index 9d73ed0..42ac998 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
@@ -89,6 +89,8 @@
     private static final String ATTR_MIGRATED_TO_POLICY_ENGINE = "migratedToPolicyEngine";
     private static final String ATTR_SECURITY_LOG_MIGRATED = "securityLogMigrated";
 
+    private static final String ATTR_MIGRATED_POST_UPGRADE = "migratedPostUpgrade";
+
     // Internal state for the device owner package.
     OwnerInfo mDeviceOwner;
     int mDeviceOwnerUserId = UserHandle.USER_NULL;
@@ -117,6 +119,8 @@
     boolean mMigratedToPolicyEngine = false;
     boolean mSecurityLoggingMigrated = false;
 
+    boolean mPoliciesMigratedPostUpdate = false;
+
     OwnersData(PolicyPathProvider pathProvider) {
         mPathProvider = pathProvider;
     }
@@ -400,6 +404,7 @@
 
             out.startTag(null, TAG_POLICY_ENGINE_MIGRATION);
             out.attributeBoolean(null, ATTR_MIGRATED_TO_POLICY_ENGINE, mMigratedToPolicyEngine);
+            out.attributeBoolean(null, ATTR_MIGRATED_POST_UPGRADE, mPoliciesMigratedPostUpdate);
             if (Flags.securityLogV2Enabled()) {
                 out.attributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, mSecurityLoggingMigrated);
             }
@@ -463,8 +468,11 @@
                 case TAG_POLICY_ENGINE_MIGRATION:
                     mMigratedToPolicyEngine = parser.getAttributeBoolean(
                             null, ATTR_MIGRATED_TO_POLICY_ENGINE, false);
+                    mPoliciesMigratedPostUpdate = parser.getAttributeBoolean(
+                            null, ATTR_MIGRATED_POST_UPGRADE, false);
                     mSecurityLoggingMigrated = Flags.securityLogV2Enabled()
                             && parser.getAttributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, false);
+
                     break;
                 default:
                     Slog.e(TAG, "Unexpected tag: " + tag);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 71facab..e713a82 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -506,6 +506,10 @@
                 UserManager.DISALLOW_SIM_GLOBALLY,
                 POLICY_FLAG_GLOBAL_ONLY_POLICY);
         USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_ASSIST_CONTENT, /* flags= */ 0);
+        if (com.android.net.thread.platform.flags.Flags.threadUserRestrictionEnabled()) {
+            USER_RESTRICTION_FLAGS.put(
+                    UserManager.DISALLOW_THREAD_NETWORK, POLICY_FLAG_GLOBAL_ONLY_POLICY);
+        }
 
         for (String key : USER_RESTRICTION_FLAGS.keySet()) {
             createAndAddUserRestrictionPolicyDefinition(key, USER_RESTRICTION_FLAGS.get(key));
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 2112dae..73d830d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1134,7 +1134,7 @@
         ServiceManager.addService(Context.PLATFORM_COMPAT_SERVICE, platformCompat);
         ServiceManager.addService(Context.PLATFORM_COMPAT_NATIVE_SERVICE,
                 new PlatformCompatNative(platformCompat));
-        AppCompatCallbacks.install(new long[0]);
+        AppCompatCallbacks.install(new long[0], new long[0]);
         t.traceEnd();
 
         // FileIntegrityService responds to requests from apps and the system. It needs to run after
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
index a33e52f..e5d3153 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
@@ -91,8 +91,8 @@
     @Test
     public void testSoftInputShowHideHistoryDump_withNulls_doesntThrow() {
         var writer = new StringWriter();
-        var history = new InputMethodManagerService.SoftInputShowHideHistory();
-        history.addEntry(new InputMethodManagerService.SoftInputShowHideHistory.Entry(
+        var history = new SoftInputShowHideHistory();
+        history.addEntry(new SoftInputShowHideHistory.Entry(
                 null,
                 null,
                 null,
diff --git a/services/tests/VpnTests/Android.bp b/services/tests/VpnTests/Android.bp
index 64a9a3b..a5011a8 100644
--- a/services/tests/VpnTests/Android.bp
+++ b/services/tests/VpnTests/Android.bp
@@ -17,8 +17,7 @@
         "java/**/*.java",
         "java/**/*.kt",
     ],
-
-    defaults: ["framework-connectivity-test-defaults"],
+    sdk_version: "core_platform", // tests can use @CorePlatformApi's
     test_suites: ["device-tests"],
     static_libs: [
         "androidx.test.rules",
@@ -32,6 +31,13 @@
         "service-connectivity-tiramisu-pre-jarjar",
     ],
     libs: [
+        // order matters: classes in framework-connectivity are resolved before framework,
+        // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
+        // stubs in framework
+        "framework-connectivity.impl",
+        "framework-connectivity-t.impl",
+        "framework",
+        "framework-res",
         "android.test.runner",
         "android.test.base",
         "android.test.mock",
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java
index dc6abf1..1c71abc 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java
@@ -52,7 +52,9 @@
     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 PORTRAIT_DOUBLE_WIDTH = new Point(2 * WIDTH, HEIGHT);
     private static final Point LANDSCAPE_SIZE = new Point(HEIGHT, WIDTH);
+    private static final Point LANDSCAPE_DOUBLE_HEIGHT = new Point(HEIGHT, 2 * WIDTH);
 
     @Mock
     private SurfaceControl.Transaction mMockTransaction;
@@ -69,6 +71,16 @@
     }
 
     @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_notRotated_anisotropyCorrection() {
+        mDisplayDeviceInfo.xDpi = 0.5f;
+        mDisplayDeviceInfo.yDpi = 1.0f;
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo,
+                mMockDisplayAdapter, /*isAnisotropyCorrectionEnabled=*/ true);
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(
+                PORTRAIT_DOUBLE_WIDTH);
+    }
+
+    @Test
     public void testGetDisplaySurfaceDefaultSizeLocked_notRotated() {
         DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo,
                 mMockDisplayAdapter);
@@ -84,6 +96,17 @@
     }
 
     @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation90_anisotropyCorrection() {
+        mDisplayDeviceInfo.xDpi = 0.5f;
+        mDisplayDeviceInfo.yDpi = 1.0f;
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo,
+                mMockDisplayAdapter, /*isAnisotropyCorrectionEnabled=*/ true);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_90, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(
+                LANDSCAPE_DOUBLE_HEIGHT);
+    }
+
+    @Test
     public void testGetDisplaySurfaceDefaultSizeLocked_rotation90() {
         DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo,
                 mMockDisplayAdapter);
@@ -111,8 +134,14 @@
         private final DisplayDeviceInfo mDisplayDeviceInfo;
 
         FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo, DisplayAdapter displayAdapter) {
+            this(displayDeviceInfo, displayAdapter, /*isAnisotropyCorrectionEnabled=*/ false);
+        }
+
+        FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo, DisplayAdapter displayAdapter,
+                boolean isAnisotropyCorrectionEnabled) {
             super(displayAdapter, /* displayToken= */ null, /* uniqueId= */ "",
-                    InstrumentationRegistry.getInstrumentation().getContext());
+                    InstrumentationRegistry.getInstrumentation().getContext(),
+                    isAnisotropyCorrectionEnabled);
             mDisplayDeviceInfo = displayDeviceInfo;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
index 1c43418..549f0d7 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
@@ -106,8 +106,184 @@
     }
 
     @Test
+    public void testLetterbox() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
+                /*isAnisotropyCorrectionEnabled=*/ false);
+        mDisplayDeviceInfo.xDpi = 0.5f;
+        mDisplayDeviceInfo.yDpi = 1.0f;
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
+        assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
+        assertEquals(DISPLAY_HEIGHT, originalDisplayInfo.logicalHeight);
+
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        assertEquals(new Point(0, 0), mLogicalDisplay.getDisplayPosition());
+
+        /*
+         * Content is too wide, should become letterboxed
+         *  ______DISPLAY_WIDTH________
+         * |                        |
+         * |________________________|
+         * |                        |
+         * |       CONTENT          |
+         * |                        |
+         * |________________________|
+         * |                        |
+         * |________________________|
+         */
+        // Make a wide application content, by reducing its height.
+        DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = DISPLAY_WIDTH;
+        displayInfo.logicalHeight = DISPLAY_HEIGHT / 2;
+        mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
+
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        assertEquals(new Point(0, DISPLAY_HEIGHT / 4), mLogicalDisplay.getDisplayPosition());
+    }
+
+    @Test
+    public void testNoLetterbox_anisotropyCorrection() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
+                /*isAnisotropyCorrectionEnabled=*/ true);
+
+        // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust
+        // to using the whole screen. This is because display will rescale it back to fill the
+        // screen (in case the display menu setting is set to stretch the pixels across the display)
+        mDisplayDeviceInfo.xDpi = 0.5f;
+        mDisplayDeviceInfo.yDpi = 1.0f;
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
+        // Content width re-scaled
+        assertEquals(DISPLAY_WIDTH * 2, originalDisplayInfo.logicalWidth);
+        assertEquals(DISPLAY_HEIGHT, originalDisplayInfo.logicalHeight);
+
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+
+        // Applications need to think that they are shown on a display with square pixels.
+        // as applications can be displayed on multiple displays simultaneously (mirrored).
+        // Content is too wide, should have become letterboxed - but it won't because of anisotropy
+        // correction
+        assertEquals(new Point(0, 0), mLogicalDisplay.getDisplayPosition());
+    }
+
+    @Test
+    public void testLetterbox_anisotropyCorrectionYDpi() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
+                /*isAnisotropyCorrectionEnabled=*/ true);
+
+        DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = DISPLAY_WIDTH;
+        displayInfo.logicalHeight = DISPLAY_HEIGHT / 2;
+        mDisplayDeviceInfo.xDpi = 1.0f;
+        mDisplayDeviceInfo.yDpi = 0.5f;
+        mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+
+        assertEquals(new Point(0, 75), mLogicalDisplay.getDisplayPosition());
+    }
+
+    @Test
+    public void testPillarbox() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
+                /*isAnisotropyCorrectionEnabled=*/ false);
+        mDisplayDeviceInfo.xDpi = 0.5f;
+        mDisplayDeviceInfo.yDpi = 1.0f;
+
+        DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.rotation = Surface.ROTATION_90;
+        displayInfo.logicalWidth = DISPLAY_WIDTH;
+        displayInfo.logicalHeight = DISPLAY_HEIGHT;
+        mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
+        mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+
+        var updatedDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
+        assertEquals(Surface.ROTATION_90, updatedDisplayInfo.rotation);
+        assertEquals(DISPLAY_WIDTH, updatedDisplayInfo.logicalWidth);
+        assertEquals(DISPLAY_HEIGHT, updatedDisplayInfo.logicalHeight);
+
+        /*
+         * Content is too tall, should become pillarboxed
+         *  ______DISPLAY_WIDTH________
+         * |    |                |    |
+         * |    |                |    |
+         * |    |                |    |
+         * |    |   CONTENT      |    |
+         * |    |                |    |
+         * |    |                |    |
+         * |____|________________|____|
+         */
+
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+
+        assertEquals(new Point(75, 0), mLogicalDisplay.getDisplayPosition());
+    }
+
+    @Test
+    public void testPillarbox_anisotropyCorrection() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
+                /*isAnisotropyCorrectionEnabled=*/ true);
+
+        DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = DISPLAY_WIDTH;
+        displayInfo.logicalHeight = DISPLAY_HEIGHT;
+        displayInfo.rotation = Surface.ROTATION_90;
+        mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
+        // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust
+        // to using the whole screen. This is because display will rescale it back to fill the
+        // screen (in case the display menu setting is set to stretch the pixels across the display)
+        mDisplayDeviceInfo.xDpi = 0.5f;
+        mDisplayDeviceInfo.yDpi = 1.0f;
+        mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+
+        // Applications need to think that they are shown on a display with square pixels.
+        // as applications can be displayed on multiple displays simultaneously (mirrored).
+        // Content is a bit wider than in #testPillarbox, due to content added stretching
+        assertEquals(new Point(50, 0), mLogicalDisplay.getDisplayPosition());
+    }
+
+    @Test
+    public void testNoPillarbox_anisotropyCorrectionYDpi() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
+                /*isAnisotropyCorrectionEnabled=*/ true);
+
+        // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust
+        // to using the whole screen. This is because display will rescale it back to fill the
+        // screen (in case the display menu setting is set to stretch the pixels across the display)
+        mDisplayDeviceInfo.xDpi = 1.0f;
+        mDisplayDeviceInfo.yDpi = 0.5f;
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
+        // Content width re-scaled
+        assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
+        assertEquals(DISPLAY_HEIGHT * 2, originalDisplayInfo.logicalHeight);
+
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+
+        // Applications need to think that they are shown on a display with square pixels.
+        // as applications can be displayed on multiple displays simultaneously (mirrored).
+        // Content is too tall, should have occupy the whole screen - but it won't because of
+        // anisotropy correction
+        assertEquals(new Point(0, 0), mLogicalDisplay.getDisplayPosition());
+    }
+
+    @Test
     public void testGetDisplayPosition() {
-        Point expectedPosition = new Point();
+        Point expectedPosition = new Point(0, 0);
 
         SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
         mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index 64076e6..3eced7f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -1631,12 +1631,25 @@
         director.start(sensorManager);
         director.injectSupportedModesByDisplay(supportedModesByDisplay);
 
-        setPeakRefreshRate(Float.POSITIVE_INFINITY);
+        // Disable Smooth Display
+        setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
 
         Vote vote1 = director.getVote(DISPLAY_ID,
                 Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
         Vote vote2 = director.getVote(DISPLAY_ID_2,
                 Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+
+        // Enable Smooth Display
+        setPeakRefreshRate(Float.POSITIVE_INFINITY);
+
+        vote1 = director.getVote(DISPLAY_ID,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
         assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, /* frameRateHigh= */ 130);
         assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, /* frameRateHigh= */ 140);
     }
@@ -1654,10 +1667,18 @@
         SensorManager sensorManager = createMockSensorManager(lightSensor);
         director.start(sensorManager);
 
-        setPeakRefreshRate(peakRefreshRate);
+        // Disable Smooth Display
+        setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
 
         Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
         assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+
+        // Enable Smooth Display
+        setPeakRefreshRate(peakRefreshRate);
+
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0,
                 /* frameRateHigh= */ peakRefreshRate);
     }
 
@@ -1759,11 +1780,23 @@
         director.start(sensorManager);
         director.injectSupportedModesByDisplay(supportedModesByDisplay);
 
-        setMinRefreshRate(Float.POSITIVE_INFINITY);
+        // Disable Force Peak Refresh Rate
+        setMinRefreshRate(0);
 
         Vote vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
         Vote vote2 = director.getVote(DISPLAY_ID_2,
                 Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+
+        // Enable Force Peak Refresh Rate
+        setMinRefreshRate(Float.POSITIVE_INFINITY);
+
+        vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
         assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 130,
                 /* frameRateHigh= */ Float.POSITIVE_INFINITY);
         assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 140,
@@ -1783,9 +1816,17 @@
         SensorManager sensorManager = createMockSensorManager(lightSensor);
         director.start(sensorManager);
 
-        setMinRefreshRate(minRefreshRate);
+        // Disable Force Peak Refresh Rate
+        setMinRefreshRate(0);
 
         Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+
+        // Enable Force Peak Refresh Rate
+        setMinRefreshRate(minRefreshRate);
+
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
         assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ minRefreshRate,
                 /* frameRateHigh= */ Float.POSITIVE_INFINITY);
     }
@@ -1829,6 +1870,58 @@
     }
 
     @Test
+    public void testPeakAndMinRefreshRate_FlagEnabled_DisplayWithOneMode() {
+        when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled())
+                .thenReturn(true);
+        DisplayModeDirector director =
+                new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags);
+        director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON);
+
+        Display.Mode[] modes1 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+                new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 130),
+        };
+        Display.Mode[] modes2 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+        };
+        SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
+        supportedModesByDisplay.put(DISPLAY_ID, modes1);
+        supportedModesByDisplay.put(DISPLAY_ID_2, modes2);
+
+        Sensor lightSensor = createLightSensor();
+        SensorManager sensorManager = createMockSensorManager(lightSensor);
+        director.start(sensorManager);
+        director.injectSupportedModesByDisplay(supportedModesByDisplay);
+
+        // Disable Force Peak Refresh Rate and Smooth Display
+        setMinRefreshRate(0);
+        setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+
+        // Even though the highest refresh rate of the second display == the current min refresh
+        // rate == 60, Force Peak Refresh Rate should remain disabled
+        Vote vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        Vote vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+
+        // Even though the highest refresh rate of the second display == the current peak refresh
+        // rate == 60, Smooth Display should remain disabled
+        vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+    }
+
+    @Test
     public void testSensorRegistration() {
         // First, configure brightness zones or DMD won't register for sensor data.
         final FakeDeviceConfig config = mInjector.getDeviceConfig();
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
index caa0864..a8b792e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
@@ -213,7 +213,7 @@
                 any(), any(), any(),
                 any(), any(),
                 any(), any(),
-                any(),
+                any(), any(),
                 anyLong(), anyLong());
 
         final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
@@ -277,7 +277,7 @@
                 null, null,
                 null,
                 null, null, null,
-                null, null,
+                null, null, null,
                 0, 0);
 
         // Sleep until timeout should have triggered
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index a2756ff..0ba74c6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -1413,6 +1413,9 @@
         final BroadcastRecord userPresentRecord2 = makeBroadcastRecord(userPresent);
 
         mImpl.enqueueBroadcastLocked(userPresentRecord1);
+        // Wait for a few ms before sending another broadcast to allow comparing the
+        // enqueue timestamps of these broadcasts.
+        SystemClock.sleep(5);
         mImpl.enqueueBroadcastLocked(userPresentRecord2);
 
         final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
index fcf761f..67be93b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
@@ -215,7 +215,7 @@
                 any(), any(), any(),
                 any(), any(),
                 any(), any(),
-                any(),
+                any(), any(),
                 anyLong(), anyLong());
         final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
         r.setPid(myPid());
@@ -263,7 +263,7 @@
                 null, null,
                 null,
                 null, null, null,
-                null, null,
+                null, null, null,
                 0, 0);
         return app;
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
index e168596..16d05b1 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
@@ -214,7 +214,7 @@
                 .thenReturn(mA11yWindowInfos.get(0));
         when(mMockA11yWindowManager.findA11yWindowInfoByIdLocked(PIP_WINDOWID))
                 .thenReturn(mA11yWindowInfos.get(1));
-        when(mMockA11yWindowManager.getDisplayIdByUserIdAndWindowIdLocked(USER_ID,
+        when(mMockA11yWindowManager.getDisplayIdByUserIdAndWindowId(USER_ID,
             WINDOWID_ONSECONDDISPLAY)).thenReturn(SECONDARY_DISPLAY_ID);
         when(mMockA11yWindowManager.getWindowListLocked(SECONDARY_DISPLAY_ID))
             .thenReturn(mA11yWindowInfosOnSecondDisplay);
diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java
index ef15f60..36b163e 100644
--- a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java
@@ -173,6 +173,25 @@
     }
 
     @Test
+    public void testGetLoggableChanges() throws Exception {
+        final long disabledChangeId = 1234L;
+        final long enabledLatestChangeId = 2345L;
+        final long enabledOlderChangeId = 3456L;
+        CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext)
+                // Disabled changes should not be logged.
+                .addDisabledChangeWithId(disabledChangeId)
+                // A change targeting the latest sdk should be logged.
+                .addEnableSinceSdkChangeWithId(3, enabledLatestChangeId)
+                // A change targeting an old sdk should not be logged.
+                .addEnableSinceSdkChangeWithId(1, enabledOlderChangeId)
+                .build();
+
+        assertThat(compatConfig.getLoggableChanges(
+                ApplicationInfoBuilder.create().withTargetSdk(3).build()))
+                    .asList().containsExactly(enabledLatestChangeId);
+    }
+
+    @Test
     public void testPackageOverrideEnabled() throws Exception {
         CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext)
                 .addDisabledChangeWithId(1234L)
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
index 1dd64ff..5582e13 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
@@ -145,6 +145,7 @@
 
     @SmallTest
     @Test
+    @Ignore("b/277916462")
     public void testCompMigrationUnAffiliated_skipped() throws Exception {
         prepareAdmin1AsDo();
         prepareAdminAnotherPackageAsPo(COPE_PROFILE_USER_ID);
@@ -216,6 +217,7 @@
 
     @SmallTest
     @Test
+    @Ignore("b/277916462")
     public void testCompMigration_keepSuspendedAppsWhenDpcIsRPlus() throws Exception {
         prepareAdmin1AsDo();
         prepareAdmin1AsPo(COPE_PROFILE_USER_ID, Build.VERSION_CODES.R);
@@ -249,6 +251,7 @@
 
     @SmallTest
     @Test
+    @Ignore("b/277916462")
     public void testCompMigration_unsuspendAppsWhenDpcNotRPlus() throws Exception {
         prepareAdmin1AsDo();
         prepareAdmin1AsPo(COPE_PROFILE_USER_ID, Build.VERSION_CODES.Q);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index dc504ca..a35a35a 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -153,7 +153,8 @@
             = SystemProperties.getBoolean("persist.debug.time_correction", true);
 
     private static final boolean USE_DEDICATED_HANDLER_THREAD =
-            SystemProperties.getBoolean("persist.debug.use_dedicated_handler_thread", false);
+            SystemProperties.getBoolean("persist.debug.use_dedicated_handler_thread",
+            Flags.useDedicatedHandlerThread());
 
     static final boolean DEBUG = false; // Never submit with true
     static final boolean DEBUG_RESPONSE_STATS = DEBUG || Log.isLoggable(TAG, Log.DEBUG);
diff --git a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
index e69b60b..c349599 100644
--- a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
+++ b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
@@ -26,5 +26,5 @@
 {
     void onQualifiedNetworkTypesChanged(int apnTypes, in int[] qualifiedNetworkTypes);
     void onNetworkValidationRequested(int networkCapability, IIntegerConsumer callback);
-    void onReconnectQualifedNetworkType(int apnTypes, int qualifiedNetworkType);
+    void onReconnectQualifiedNetworkType(int apnTypes, int qualifiedNetworkType);
 }
diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java
index 7bfe04d..f775de6 100644
--- a/telephony/java/android/telephony/data/QualifiedNetworksService.java
+++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java
@@ -238,7 +238,7 @@
                 @AccessNetworkConstants.RadioAccessNetworkType int qualifiedNetworkType) {
             if (mCallback != null) {
                 try {
-                    mCallback.onReconnectQualifedNetworkType(apnTypes, qualifiedNetworkType);
+                    mCallback.onReconnectQualifiedNetworkType(apnTypes, qualifiedNetworkType);
                 } catch (RemoteException e) {
                     loge("Failed to call onReconnectQualifiedNetworkType. " + e);
                 }
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 755636a..75284c7 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -43,13 +43,13 @@
 import android.os.test.TestLooper;
 import android.provider.DeviceConfig;
 import android.util.AtomicFile;
-import android.util.LongArrayQueue;
 import android.util.Xml;
+import android.utils.LongArrayQueue;
+import android.utils.XmlUtils;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
-import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.PackageWatchdog.HealthCheckState;