Merge "Move caption insets WCT to the shell bg thread" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
index dc5e341..93904a7 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.os.Environment;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
@@ -113,15 +114,18 @@
     }
 
     /**
-     * Add user wakeup for the alarm.
+     * Add user wakeup for the alarm if needed.
      * @param userId Id of the user that scheduled alarm.
      * @param alarmTime time when alarm is expected to trigger.
      */
     public void addUserWakeup(int userId, long alarmTime) {
-        synchronized (mUserWakeupLock) {
-            mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+        // SYSTEM user is always running, so no need to schedule wakeup for it.
+        if (userId != UserHandle.USER_SYSTEM) {
+            synchronized (mUserWakeupLock) {
+                mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+            }
+            updateUserListFile();
         }
-        updateUserListFile();
     }
 
     /**
diff --git a/core/api/current.txt b/core/api/current.txt
index 0f23721..d610f4c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -26569,7 +26569,7 @@
 package android.media.projection {
 
   public final class MediaProjection {
-    method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
+    method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
     method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler);
     method public void stop();
     method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 44c4ab4..e0c3230 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -618,6 +618,7 @@
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void resetDefaultCrossProfileIntentFilters(int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void resetShouldAllowBypassingDevicePolicyManagementRoleQualificationState();
     method @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int);
+    method @FlaggedApi("android.app.admin.flags.provisioning_context_parameter") @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int, @Nullable String);
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwner(@NonNull android.content.ComponentName, int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwnerOnly(@NonNull android.content.ComponentName, int);
     method public void setDeviceOwnerType(@NonNull android.content.ComponentName, int);
diff --git a/core/java/android/animation/OWNERS b/core/java/android/animation/OWNERS
index f3b330a..5223c87 100644
--- a/core/java/android/animation/OWNERS
+++ b/core/java/android/animation/OWNERS
@@ -3,3 +3,4 @@
 romainguy@google.com
 tianliu@google.com
 adamp@google.com
+mount@google.com
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index fb0ce0d..a7070b9 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -9202,6 +9202,14 @@
     /**
      * @hide
      */
+    @UnsupportedAppUsage
+    public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) {
+        setActiveAdmin(policyReceiver, refreshing, myUserId());
+    }
+
+    /**
+     * @hide
+     */
     @TestApi
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     @RequiresPermission(allOf = {
@@ -9210,21 +9218,45 @@
     })
     public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing,
             int userHandle) {
-        if (mService != null) {
-            try {
-                mService.setActiveAdmin(policyReceiver, refreshing, userHandle);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-        }
+        setActiveAdminInternal(policyReceiver, refreshing, userHandle, null);
     }
 
     /**
      * @hide
      */
-    @UnsupportedAppUsage
-    public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) {
-        setActiveAdmin(policyReceiver, refreshing, myUserId());
+    @TestApi
+    @RequiresPermission(allOf = {
+            MANAGE_DEVICE_ADMINS,
+            INTERACT_ACROSS_USERS_FULL
+    })
+    @FlaggedApi(Flags.FLAG_PROVISIONING_CONTEXT_PARAMETER)
+    public void setActiveAdmin(
+            @NonNull ComponentName policyReceiver,
+            boolean refreshing,
+            int userHandle,
+            @Nullable String provisioningContext
+    ) {
+        setActiveAdminInternal(policyReceiver, refreshing, userHandle, provisioningContext);
+    }
+
+    private void setActiveAdminInternal(
+            @NonNull ComponentName policyReceiver,
+            boolean refreshing,
+            int userHandle,
+            @Nullable String provisioningContext
+    ) {
+        if (mService != null) {
+            try {
+                mService.setActiveAdmin(
+                        policyReceiver,
+                        refreshing,
+                        userHandle,
+                        provisioningContext
+                );
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
@@ -9678,7 +9710,7 @@
         if (mService != null) {
             try {
                 final int myUserId = myUserId();
-                mService.setActiveAdmin(admin, false, myUserId);
+                mService.setActiveAdmin(admin, false, myUserId, null);
                 return mService.setProfileOwner(admin, myUserId);
             } catch (RemoteException re) {
                 throw re.rethrowFromSystemServer();
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index d183713..381f996 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -160,7 +160,8 @@
     void setKeyguardDisabledFeatures(in ComponentName who, String callerPackageName, int which, boolean parent);
     int getKeyguardDisabledFeatures(in ComponentName who, int userHandle, boolean parent);
 
-    void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, int userHandle);
+    void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing,
+        int userHandle, String provisioningContext);
     boolean isAdminActive(in ComponentName policyReceiver, int userHandle);
     List<ComponentName> getActiveAdmins(int userHandle);
     @UnsupportedAppUsage
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 8227112..c789af3 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -393,3 +393,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "provisioning_context_parameter"
+    namespace: "enterprise"
+    description: "Add provisioningContext to store metadata about when the admin was set"
+    bug: "326525847"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index c2c7b81..5a39702 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -327,3 +327,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "fix_large_display_private_space_settings"
+  namespace: "profile_experiences"
+  description: "Fix tablet and foldable specific bugs for private space"
+  bug: "342563741"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 1d950dc..6343313 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -119,9 +119,11 @@
 
     @Override
     public boolean applyLocalVisibilityOverride() {
-        ImeTracing.getInstance().triggerClientDump(
-                "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
-                mController.getHost().getInputMethodManager(), null /* icProto */);
+        if (!Flags.refactorInsetsController()) {
+            ImeTracing.getInstance().triggerClientDump(
+                    "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+                    mController.getHost().getInputMethodManager(), null /* icProto */);
+        }
         return super.applyLocalVisibilityOverride();
     }
 
@@ -205,9 +207,13 @@
 
     @Override
     public void removeSurface() {
-        final IBinder window = mController.getHost().getWindowToken();
-        if (window != null) {
-            getImm().removeImeSurface(window);
+        if (Flags.refactorInsetsController()) {
+            super.removeSurface();
+        } else {
+            final IBinder window = mController.getHost().getWindowToken();
+            if (window != null) {
+                getImm().removeImeSurface(window);
+            }
         }
     }
 
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index df2af73..f166b89 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -765,7 +765,7 @@
 
     public InsetsController(Host host) {
         this(host, (controller, id, type) -> {
-            if (type == ime()) {
+            if (!Flags.refactorInsetsController() &&  type == ime()) {
                 return new ImeInsetsSourceConsumer(id, controller.mState,
                         Transaction::new, controller);
             } else {
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index c73cbc6..477e35b 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -43,6 +43,7 @@
 import android.view.inputmethod.ImeTracker;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.ImeTracing;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -296,6 +297,13 @@
 
     @VisibleForTesting(visibility = PACKAGE)
     public boolean applyLocalVisibilityOverride() {
+        if (Flags.refactorInsetsController()) {
+            if (mType == WindowInsets.Type.ime()) {
+                ImeTracing.getInstance().triggerClientDump(
+                        "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+                        mController.getHost().getInputMethodManager(), null /* icProto */);
+            }
+        }
         final InsetsSource source = mState.peekSource(mId);
         if (source == null) {
             return false;
@@ -396,6 +404,14 @@
      */
     public void removeSurface() {
         // no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
+        if (Flags.refactorInsetsController()) {
+            if (mType == WindowInsets.Type.ime()) {
+                final IBinder window = mController.getHost().getWindowToken();
+                if (window != null) {
+                    mController.getHost().getInputMethodManager().removeImeSurface(window);
+                }
+            }
+        }
     }
 
     @VisibleForTesting(visibility = PACKAGE)
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index c9d2eec..fed8eea 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -1352,12 +1352,16 @@
                 case MSG_SET_VISIBILITY:
                     final boolean visible = msg.arg1 != 0;
                     synchronized (mH) {
-                        if (visible) {
-                            showSoftInput(mServedView, /* flags */ 0);
-                        } else {
-                            if (mCurRootView != null
-                                    && mCurRootView.getInsetsController() != null) {
-                                mCurRootView.getInsetsController().hide(WindowInsets.Type.ime());
+                        if (mCurRootView != null) {
+                            final var insetsController = mCurRootView.getInsetsController();
+                            if (insetsController != null) {
+                                if (visible) {
+                                    insetsController.show(WindowInsets.Type.ime(),
+                                            false /* fromIme */, null /* statsToken */);
+                                } else {
+                                    insetsController.hide(WindowInsets.Type.ime(),
+                                            false /* fromIme */, null /* statsToken */);
+                                }
                             }
                         }
                     }
@@ -2334,16 +2338,18 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
 
             if (Flags.refactorInsetsController()) {
+                final var viewRootImpl = view.getViewRootImpl();
                 // In case of a running show IME animation, it should not be requested visible,
                 // otherwise the animation would jump and not be controlled by the user anymore
-                if ((mCurRootView.getInsetsController().computeUserAnimatingTypes()
-                        & WindowInsets.Type.ime()) == 0) {
+                if (viewRootImpl != null
+                        && (viewRootImpl.getInsetsController().computeUserAnimatingTypes()
+                                & WindowInsets.Type.ime()) == 0) {
                     // TODO(b/322992891) handle case of SHOW_IMPLICIT
-                    view.getWindowInsetsController().show(WindowInsets.Type.ime());
+                    viewRootImpl.getInsetsController().show(WindowInsets.Type.ime(),
+                            false /* fromIme */, statsToken);
                     return true;
-                } else {
-                    return false;
                 }
+                return false;
             } else {
                 // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
                 // TODO(b/229426865): call WindowInsetsController#show instead.
@@ -2497,7 +2503,10 @@
 
             if (Flags.refactorInsetsController()) {
                 // TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY
-                servedView.getWindowInsetsController().hide(WindowInsets.Type.ime());
+                final var viewRootImpl = servedView.getViewRootImpl();
+                if (viewRootImpl != null) {
+                    viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime());
+                }
                 return true;
             } else {
                 return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken,
diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java
index 6629fdc4..16727c3 100644
--- a/core/java/android/webkit/WebViewProviderInfo.java
+++ b/core/java/android/webkit/WebViewProviderInfo.java
@@ -23,6 +23,9 @@
 import android.os.Parcelable;
 import android.util.Base64;
 
+import java.util.Arrays;
+import java.util.Objects;
+
 /**
  * @hide
  */
@@ -80,6 +83,35 @@
         out.writeTypedArray(signatures, 0);
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o instanceof WebViewProviderInfo that) {
+            return this.packageName.equals(that.packageName)
+                    && this.description.equals(that.description)
+                    && this.availableByDefault == that.availableByDefault
+                    && this.isFallback == that.isFallback
+                    && Arrays.equals(this.signatures, that.signatures);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(packageName, description, availableByDefault,
+                isFallback, Arrays.hashCode(signatures));
+    }
+
+    @Override
+    public String toString() {
+        return "WebViewProviderInfo; packageName=" + packageName
+                + " description=\"" + description
+                + "\" availableByDefault=" + availableByDefault
+                + " isFallback=" + isFallback
+                + " signatures=" + Arrays.toString(signatures);
+    }
+
     // fields read from framework resource
     public final String packageName;
     public final String description;
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 9024569..91ac4ff 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -101,6 +101,13 @@
 }
 
 flag {
+    name: "enable_cascading_windows"
+    namespace: "lse_desktop_experience"
+    description: "Whether to apply cascading effect for placing multiple windows when first launched"
+    bug: "325240051"
+}
+
+flag {
     name: "enable_camera_compat_for_desktop_windowing"
     namespace: "lse_desktop_experience"
     description: "Whether to apply Camera Compat treatment to fixed-orientation apps in desktop windowing mode"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 5397e91..c451cc8 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -19,6 +19,16 @@
 }
 
 flag {
+    name: "do_not_skip_ime_by_target_visibility"
+    namespace: "windowing_frontend"
+    description: "Avoid window traversal missing IME"
+    bug: "339375944"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "apply_lifecycle_on_pip_change"
     namespace: "windowing_frontend"
     description: "Make pip activity lifecyle change with windowing mode"
diff --git a/core/java/com/android/internal/protolog/IProtoLogClient.aidl b/core/java/com/android/internal/protolog/IProtoLogClient.aidl
new file mode 100644
index 0000000..969ed99
--- /dev/null
+++ b/core/java/com/android/internal/protolog/IProtoLogClient.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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.internal.protolog;
+
+/**
+ * The ProtoLog client interface.
+ *
+ * These clients will communicate bi-directionally with the ProtoLog service
+ * (@see IProtoLogService.aidl) running in the system process.
+ *
+ * {@hide}
+ */
+interface IProtoLogClient {
+    void toggleLogcat(boolean enabled, in String[] groups);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/protolog/IProtoLogService.aidl b/core/java/com/android/internal/protolog/IProtoLogService.aidl
new file mode 100644
index 0000000..cc349ea
--- /dev/null
+++ b/core/java/com/android/internal/protolog/IProtoLogService.aidl
@@ -0,0 +1,51 @@
+/*
+ * 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.internal.protolog;
+
+import com.android.internal.protolog.IProtoLogClient;
+
+/**
+ * The ProtoLog Service interface.
+ *
+ * This service runs in system server.
+ *
+ * All ProtoLog clients (in theory one per process) will register themselves to this service.
+ * This service will then serve as the entry point for any shell command based interactions with
+ * protolog and get/forward any required information/actions from/to all the registered ProtoLog
+ * clients.
+ *
+ * Clients will be responsible for directly sending all their trace data to Perfetto without passing
+ * through this service, except viewer config data, the mappings from messages hashes (generated by
+ * the ProtoLog Tool if and when the tool processed the source code). So, this service is
+ * responsible for dumping the viewer config data from all clients. The reason for this is because
+ * we do this on Perfetto's onFlush callback when a tracing instance is stopped. However, if a
+ * client process is frozen then this will not dump; system server, where this service runs cannot
+ * be frozen. Secondly many processes (i.e. apps) will run the same code with the same viewer config
+ * data, so this service allows us to orchestrate which config gets dumped so we don't duplicate
+ * this information in the trace and waste valuable trace space.
+ *
+ * {@hide}
+ */
+interface IProtoLogService {
+    interface IRegisterClientArgs {
+        String[] getGroups();
+        boolean[] getGroupsDefaultLogcatStatus();
+        String getViewerConfigFile();
+    }
+
+    void registerClient(IProtoLogClient client, IRegisterClientArgs args);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 652cba7..f937700 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -88,7 +88,7 @@
 /**
  * A service for the ProtoLog logging system.
  */
-public class PerfettoProtoLogImpl implements IProtoLog {
+public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog {
     private static final String LOG_TAG = "ProtoLog";
     public static final String NULL_STRING = "null";
     private final AtomicInteger mTracingInstances = new AtomicInteger();
@@ -100,6 +100,7 @@
     );
     @Nullable
     private final ProtoLogViewerConfigReader mViewerConfigReader;
+    @Nullable
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
     private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
     private final Runnable mCacheUpdater;
@@ -111,13 +112,12 @@
     private final Lock mBackgroundServiceLock = new ReentrantLock();
     private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
 
-    public PerfettoProtoLogImpl(String viewerConfigFilePath, Runnable cacheUpdater) {
+    public PerfettoProtoLogImpl(@NonNull String viewerConfigFilePath, Runnable cacheUpdater) {
         this(() -> {
             try {
                 return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
             } catch (FileNotFoundException e) {
-                Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e);
-                return null;
+                throw new RuntimeException("Failed to load viewer config file " + viewerConfigFilePath, e);
             }
         }, cacheUpdater);
     }
@@ -127,7 +127,7 @@
     }
 
     public PerfettoProtoLogImpl(
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
             Runnable cacheUpdater
     ) {
         this(viewerConfigInputStreamProvider,
@@ -203,6 +203,15 @@
         return mTracingInstances.get() > 0;
     }
 
+    @Override
+    public void toggleLogcat(boolean enabled, String[] groups) {
+        if (enabled) {
+            startLoggingToLogcat(groups, null);
+        } else {
+            stopLoggingToLogcat(groups, null);
+        }
+    }
+
     /**
      * Start text logging
      * @param groups Groups to start text logging for
@@ -242,6 +251,15 @@
         for (IProtoLogGroup protoLogGroup : protoLogGroups) {
             mLogGroups.put(protoLogGroup.name(), protoLogGroup);
         }
+
+        final String[] groupsLoggingToLogcat = Arrays.stream(protoLogGroups)
+                .filter(IProtoLogGroup::isLogToLogcat)
+                .map(IProtoLogGroup::name)
+                .toArray(String[]::new);
+
+        if (mViewerConfigReader != null) {
+            mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat);
+        }
     }
 
     /**
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 3082295..77ca7ce 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -30,6 +30,7 @@
 import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.common.ProtoLogToolInjected;
 
+import java.io.File;
 import java.util.TreeMap;
 
 /**
@@ -105,7 +106,15 @@
     public static synchronized IProtoLog getSingleInstance() {
         if (sServiceInstance == null) {
             if (android.tracing.Flags.perfettoProtologTracing()) {
-                sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater);
+                File f = new File(sViewerConfigPath);
+                if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) {
+                    // TODO(b/353530422): Remove - temporary fix to unblock b/352290057
+                    // In so tests the viewer config file might not exist in which we don't
+                    // want to provide config path to the user
+                    sServiceInstance = new PerfettoProtoLogImpl(null, null, sCacheUpdater);
+                } else {
+                    sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater);
+                }
             } else {
                 sServiceInstance = new LegacyProtoLogImpl(
                         sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater);
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index bb6c8b7..38ca0d8 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -24,12 +24,13 @@
 import java.util.TreeMap;
 
 public class ProtoLogViewerConfigReader {
+    @NonNull
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
     private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>();
     private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>();
 
     public ProtoLogViewerConfigReader(
-            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
         this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
     }
 
diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
index 334f548..14bc8e4 100644
--- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
+++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
@@ -16,11 +16,13 @@
 
 package com.android.internal.protolog;
 
+import android.annotation.NonNull;
 import android.util.proto.ProtoInputStream;
 
 public interface ViewerConfigInputStreamProvider {
     /**
      * @return a ProtoInputStream.
      */
+    @NonNull
     ProtoInputStream getInputStream();
 }
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index c07fd38..7c62615 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -27,6 +27,7 @@
 #include <android_media_audiopolicy.h>
 #include <android_os_Parcel.h>
 #include <audiomanager/AudioManager.h>
+#include <android-base/properties.h>
 #include <binder/IBinder.h>
 #include <jni.h>
 #include <media/AidlConversion.h>
@@ -41,8 +42,10 @@
 #include <system/audio_policy.h>
 #include <utils/Log.h>
 
+#include <thread>
 #include <optional>
 #include <sstream>
+#include <memory>
 #include <vector>
 
 #include "android_media_AudioAttributes.h"
@@ -261,6 +264,13 @@
     jfieldID mMixerBehavior;
 } gAudioMixerAttributesField;
 
+static struct {
+    jclass clazz;
+    jmethodID run;
+} gRunnableClassInfo;
+
+static JavaVM* gVm;
+
 static Mutex gLock;
 
 enum AudioError {
@@ -3362,6 +3372,55 @@
     return enabled;
 }
 
+class JavaSystemPropertyListener {
+  public:
+    JavaSystemPropertyListener(JNIEnv* env, jobject javaCallback, std::string sysPropName) :
+            mCallback(env->NewGlobalRef(javaCallback)),
+            mCachedProperty(android::base::CachedProperty{std::move(sysPropName)}) {
+        mListenerThread = std::thread([this]() mutable {
+            JNIEnv* threadEnv = GetOrAttachJNIEnvironment(gVm);
+            while (!mCleanupSignal.load()) {
+                using namespace std::chrono_literals;
+                // 1s timeout so this thread can read the cleanup signal to (slowly) be able to
+                // be destroyed.
+                std::string newVal = mCachedProperty.WaitForChange(1000ms) ?: "";
+                if (newVal != "" && mLastVal != newVal) {
+                    threadEnv->CallVoidMethod(mCallback, gRunnableClassInfo.run);
+                    mLastVal = std::move(newVal);
+                }
+            }
+            });
+    }
+
+    ~JavaSystemPropertyListener() {
+        mCleanupSignal.store(true);
+        mListenerThread.join();
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm);
+        env->DeleteGlobalRef(mCallback);
+    }
+
+  private:
+    jobject mCallback;
+    android::base::CachedProperty mCachedProperty;
+    std::thread mListenerThread;
+    std::atomic<bool> mCleanupSignal{false};
+    std::string mLastVal = "";
+};
+
+std::vector<std::unique_ptr<JavaSystemPropertyListener>> gSystemPropertyListeners;
+std::mutex gSysPropLock{};
+
+static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env,  jobject thiz,
+        jstring sysProp,
+        jobject javaCallback) {
+    ScopedUtfChars sysPropChars{env, sysProp};
+    auto listener = std::make_unique<JavaSystemPropertyListener>(env, javaCallback,
+            std::string{sysPropChars.c_str()});
+    std::unique_lock _l{gSysPropLock};
+    gSystemPropertyListeners.push_back(std::move(listener));
+}
+
+
 // ----------------------------------------------------------------------------
 
 #define MAKE_AUDIO_SYSTEM_METHOD(x) \
@@ -3534,7 +3593,12 @@
                                 android_media_AudioSystem_clearPreferredMixerAttributes),
          MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency),
          MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled),
-         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled)};
+         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled),
+         MAKE_JNI_NATIVE_METHOD("listenForSystemPropertyChange",
+                                "(Ljava/lang/String;Ljava/lang/Runnable;)V",
+                                android_media_AudioSystem_listenForSystemPropertyChange),
+
+        };
 
 static const JNINativeMethod gEventHandlerMethods[] =
         {MAKE_JNI_NATIVE_METHOD("native_setup", "(Ljava/lang/Object;)V",
@@ -3816,6 +3880,12 @@
     gAudioMixerAttributesField.mMixerBehavior =
             GetFieldIDOrDie(env, audioMixerAttributesClass, "mMixerBehavior", "I");
 
+    jclass runnableClazz = FindClassOrDie(env, "java/lang/Runnable");
+    gRunnableClassInfo.clazz = MakeGlobalRefOrDie(env, runnableClazz);
+    gRunnableClassInfo.run = GetMethodIDOrDie(env, runnableClazz, "run", "()V");
+
+    LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&gVm) != 0);
+
     AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback);
 
     RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index d32486c..240be3f 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -415,7 +415,7 @@
         env->DeleteLocalRef(pointerCoordsObj);
     }
 
-    event->addSample(eventTimeNanos, rawPointerCoords.data());
+    event->addSample(eventTimeNanos, rawPointerCoords.data(), event->getId());
     event->setMetaState(event->getMetaState() | metaState);
 }
 
diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp
new file mode 100644
index 0000000..1fb5f2c
--- /dev/null
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp
@@ -0,0 +1,30 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "BatteryUsageStatsProtoTests",
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.rules",
+        "junit",
+        "mockito-target-minus-junit4",
+        "platform-test-annotations",
+        "platformprotosnano",
+        "statsdprotolite",
+        "truth",
+    ],
+
+    libs: ["android.test.runner"],
+
+    platform_apis: true,
+    certificate: "platform",
+
+    test_suites: ["device-tests"],
+}
diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml
new file mode 100644
index 0000000..9128dca
--- /dev/null
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.frameworks.core.batteryusagestatsprototests">
+
+    <uses-permission android:name="android.permission.BATTERY_STATS"/>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.core.batteryusagestatsprototests"
+        android:label="BatteryUsageStats Proto Tests" />
+
+</manifest>
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
similarity index 73%
rename from services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
rename to core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
index 62efbc3..ac1f7d0 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.server.power.stats;
+package com.android.internal.os;
 
 import static android.os.BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE;
 
@@ -23,262 +23,39 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import android.os.AggregateBatteryConsumer;
 import android.os.BatteryConsumer;
 import android.os.BatteryUsageStats;
-import android.os.Process;
 import android.os.UidBatteryConsumer;
 import android.os.nano.BatteryUsageStatsAtomsProto;
 import android.os.nano.BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage;
-import android.platform.test.ravenwood.RavenwoodRule;
-import android.util.StatsEvent;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.server.am.BatteryStatsService;
-
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
-import org.junit.Rule;
 import org.junit.Test;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-@SmallTest
-public class BatteryUsageStatsAtomTest {
 
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+@SmallTest
+public class BatteryUsageStatsPulledTest {
 
     private static final int UID_0 = 1000;
     private static final int UID_1 = 2000;
     private static final int UID_2 = 3000;
     private static final int UID_3 = 4000;
+    private static final int[] UID_USAGE_TIME_PROCESS_STATES = {
+            BatteryConsumer.PROCESS_STATE_FOREGROUND,
+            BatteryConsumer.PROCESS_STATE_BACKGROUND,
+            BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE
+    };
 
     @Test
-    public void testAtom_BatteryUsageStatsPerUid() {
-        final BatteryUsageStats bus = buildBatteryUsageStats();
-        BatteryStatsService.FrameworkStatsLogger statsLogger =
-                mock(BatteryStatsService.FrameworkStatsLogger.class);
-
-        List<StatsEvent> actual = new ArrayList<>();
-        new BatteryStatsService.StatsPerUidLogger(statsLogger).logStats(bus, actual);
-
-        // Device-wide totals
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                Process.INVALID_UID,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "cpu",
-                30000.0f,
-                20100.0f,
-                20300L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                Process.INVALID_UID,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "camera",
-                30000.0f,
-                20150.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                Process.INVALID_UID,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "CustomConsumer1",
-                30000.0f,
-                20200.0f,
-                20400L
-        );
-
-        // Per-proc state estimates for UID_0
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "screen",
-                1650.0f,
-                300.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "cpu",
-                1650.0f,
-                400.0f,
-                600L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                1000L,
-                "cpu",
-                1650.0f,
-                9100.0f,
-                8100L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                2000L,
-                "cpu",
-                1650.0f,
-                9200.0f,
-                8200L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE,
-                0L,
-                "cpu",
-                1650.0f,
-                9300.0f,
-                8400L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_CACHED,
-                0L,
-                "cpu",
-                1650.0f,
-                9400.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                1000L,
-                "CustomConsumer1",
-                1650.0f,
-                450.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                2000L,
-                "CustomConsumer1",
-                1650.0f,
-                450.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                1000L,
-                "CustomConsumer2",
-                1650.0f,
-                500.0f,
-                800L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                2000L,
-                "CustomConsumer2",
-                1650.0f,
-                500.0f,
-                800L
-        );
-
-        // Nothing for UID_1, because its power consumption is 0
-
-        // Only "screen" is populated for UID_2
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_2,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "screen",
-                766.0f,
-                766.0f,
-                0L
-        );
-
-        verifyNoMoreInteractions(statsLogger);
-    }
-
-    @Test
-    public void testAtom_BatteryUsageStatsAtomsProto() {
+    public void testGetStatsProto() {
         final BatteryUsageStats bus = buildBatteryUsageStats();
         final byte[] bytes = bus.getStatsProto();
         BatteryUsageStatsAtomsProto proto;
@@ -291,7 +68,9 @@
 
         assertEquals(bus.getStatsStartTimestamp(), proto.sessionStartMillis);
         assertEquals(bus.getStatsEndTimestamp(), proto.sessionEndMillis);
-        assertEquals(10000, proto.sessionDurationMillis);
+        assertEquals(
+                bus.getStatsEndTimestamp() - bus.getStatsStartTimestamp(),
+                proto.sessionDurationMillis);
         assertEquals(bus.getDischargePercentage(), proto.sessionDischargePercentage);
         assertEquals(bus.getDischargeDurationMs(), proto.dischargeDurationMillis);
 
@@ -311,8 +90,8 @@
         final List<android.os.UidBatteryConsumer> uidConsumers = bus.getUidBatteryConsumers();
         uidConsumers.sort((a, b) -> a.getUid() - b.getUid());
 
-        final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto =
-                proto.uidBatteryConsumers;
+        final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto
+                = proto.uidBatteryConsumers;
         Arrays.sort(uidConsumersProto, (a, b) -> a.uid - b.uid);
 
         // UID_0 - After sorting, UID_0 should be in position 0 for both data structures
@@ -407,12 +186,6 @@
         }
     }
 
-    private static final int[] UID_USAGE_TIME_PROCESS_STATES = {
-            BatteryConsumer.PROCESS_STATE_FOREGROUND,
-            BatteryConsumer.PROCESS_STATE_BACKGROUND,
-            BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE
-    };
-
     private void assertSameUidBatteryConsumer(
             android.os.UidBatteryConsumer uidConsumer,
             BatteryUsageStatsAtomsProto.UidBatteryConsumer uidConsumerProto,
@@ -422,10 +195,10 @@
         assertEquals("Uid consumers had mismatched uids", uid, uidConsumer.getUid());
 
         assertEquals("For uid " + uid,
-                uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_FOREGROUND),
+                uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND),
                 uidConsumerProto.timeInForegroundMillis);
         assertEquals("For uid " + uid,
-                uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_BACKGROUND),
+                uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND),
                 uidConsumerProto.timeInBackgroundMillis);
         for (int processState : UID_USAGE_TIME_PROCESS_STATES) {
             final long timeInStateMillis = uidConsumer.getTimeInProcessStateMs(processState);
@@ -492,9 +265,7 @@
                         .setDischargePercentage(20)
                         .setDischargedPowerRange(1000, 2000)
                         .setDischargeDurationMs(1234)
-                        .setStatsStartTimestamp(1000)
-                        .setStatsEndTimestamp(20000)
-                        .setStatsDuration(10000);
+                        .setStatsStartTimestamp(1000);
         final UidBatteryConsumer.Builder uidBuilder = builder
                 .getOrCreateUidBatteryConsumerBuilder(UID_0)
                 .setPackageWithHighestDrain("myPackage0")
diff --git a/core/tests/coretests/src/android/animation/OWNERS b/core/tests/coretests/src/android/animation/OWNERS
new file mode 100644
index 0000000..1eefb3a
--- /dev/null
+++ b/core/tests/coretests/src/android/animation/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/animation/OWNERS
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 47b28899..248db65 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -37,8 +37,12 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.view.WindowManager.BadTokenException;
 import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.ImeTracker;
 import android.widget.TextView;
 
@@ -46,6 +50,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
@@ -61,6 +66,9 @@
 @RunWith(AndroidJUnit4.class)
 public class ImeInsetsSourceConsumerTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
     InsetsSourceConsumer mImeConsumer;
     @Spy InsetsController mController;
@@ -112,6 +120,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeRequestedVisibleAwaitingControl() {
         // Set null control and then request show.
         mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -141,6 +150,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeRequestedVisibleAwaitingLeash() {
         // Set null control, then request show.
         mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -185,6 +195,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeGetAndClearSkipAnimationOnce_expectSkip() {
         // Expect IME animation will skipped when the IME is visible at first place.
         verifyImeGetAndClearSkipAnimationOnce(true /* hasWindowFocus */, true /* hasViewFocus */,
@@ -192,6 +203,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeGetAndClearSkipAnimationOnce_expectNoSkip() {
         // Expect IME animation will not skipped if previously no view focused when gained the
         // window focus and requesting the IME visible next time.
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
index 7c7cb18..9887c27 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
@@ -55,9 +55,9 @@
 import com.sun.tools.javac.code.Symbol;
 import com.sun.tools.javac.code.Symbol.ClassSymbol;
 import com.sun.tools.javac.code.Symbol.MethodSymbol;
-import com.sun.tools.javac.code.Symbol.VarSymbol;
 import com.sun.tools.javac.code.Type;
 import com.sun.tools.javac.code.Type.ClassType;
+import com.sun.tools.javac.tree.JCTree.JCNewClass;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -67,7 +67,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
 import java.util.regex.Pattern;
 
 import javax.lang.model.element.Name;
@@ -125,6 +124,12 @@
             instanceMethod()
                     .onDescendantOf("android.content.Context")
                     .withNameMatching(Pattern.compile("^send(Ordered|Sticky)?Broadcast.*$")));
+    private static final Matcher<ExpressionTree> SEND_BROADCAST_AS_USER =
+            methodInvocation(
+                    instanceMethod()
+                            .onDescendantOf("android.content.Context")
+                            .withNameMatching(
+                                    Pattern.compile("^send(Ordered|Sticky)?Broadcast.*AsUser.*$")));
     private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation(
             instanceMethod()
                     .onDescendantOf("android.app.PendingIntent")
@@ -306,18 +311,6 @@
         }
     }
 
-    private static ExpressionTree findArgumentByParameterName(MethodInvocationTree tree,
-            Predicate<String> paramName) {
-        final MethodSymbol sym = ASTHelpers.getSymbol(tree);
-        final List<VarSymbol> params = sym.getParameters();
-        for (int i = 0; i < params.size(); i++) {
-            if (paramName.test(params.get(i).name.toString())) {
-                return tree.getArguments().get(i);
-            }
-        }
-        return null;
-    }
-
     private static Name resolveName(ExpressionTree tree) {
         if (tree instanceof IdentifierTree) {
             return ((IdentifierTree) tree).getName();
@@ -345,76 +338,85 @@
 
     private static ParsedRequiresPermission parseBroadcastSourceRequiresPermission(
             MethodInvocationTree methodTree, VisitorState state) {
-        final ExpressionTree arg = findArgumentByParameterName(methodTree,
-                (name) -> name.toLowerCase().contains("intent"));
-        if (arg instanceof IdentifierTree) {
-            final Name argName = ((IdentifierTree) arg).getName();
-            final MethodTree method = state.findEnclosing(MethodTree.class);
-            final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>();
-            method.accept(new TreeScanner<Void, Void>() {
-                private ParsedRequiresPermission last;
+        if (methodTree.getArguments().size() < 1) {
+            return null;
+        }
+        final ExpressionTree arg = methodTree.getArguments().get(0);
+        if (!(arg instanceof IdentifierTree)) {
+            return null;
+        }
+        final Name argName = ((IdentifierTree) arg).getName();
+        final MethodTree method = state.findEnclosing(MethodTree.class);
+        final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>();
+        method.accept(new TreeScanner<Void, Void>() {
+            private ParsedRequiresPermission mLast;
 
-                @Override
-                public Void visitMethodInvocation(MethodInvocationTree tree, Void param) {
-                    if (Objects.equal(methodTree, tree)) {
-                        res.set(last);
-                    } else {
-                        final Name name = resolveName(tree.getMethodSelect());
-                        if (Objects.equal(argName, name)
-                                && INTENT_SET_ACTION.matches(tree, state)) {
-                            last = parseIntentAction(tree);
+            @Override
+            public Void visitMethodInvocation(MethodInvocationTree tree, Void param) {
+                if (Objects.equal(methodTree, tree)) {
+                    res.set(mLast);
+                } else {
+                    final Name name = resolveName(tree.getMethodSelect());
+                    if (Objects.equal(argName, name) && INTENT_SET_ACTION.matches(tree, state)) {
+                        mLast = parseIntentAction(tree);
+                    } else if (name == null && tree.getMethodSelect() instanceof MemberSelectTree) {
+                        ExpressionTree innerTree =
+                                ((MemberSelectTree) tree.getMethodSelect()).getExpression();
+                        if (innerTree instanceof JCNewClass) {
+                            mLast = parseIntentAction((NewClassTree) innerTree);
                         }
                     }
-                    return super.visitMethodInvocation(tree, param);
                 }
+                return super.visitMethodInvocation(tree, param);
+            }
 
-                @Override
-                public Void visitAssignment(AssignmentTree tree, Void param) {
-                    final Name name = resolveName(tree.getVariable());
-                    final Tree init = tree.getExpression();
-                    if (Objects.equal(argName, name)
-                            && init instanceof NewClassTree) {
-                        last = parseIntentAction((NewClassTree) init);
-                    }
-                    return super.visitAssignment(tree, param);
+            @Override
+            public Void visitAssignment(AssignmentTree tree, Void param) {
+                final Name name = resolveName(tree.getVariable());
+                final Tree init = tree.getExpression();
+                if (Objects.equal(argName, name) && init instanceof NewClassTree) {
+                    mLast = parseIntentAction((NewClassTree) init);
                 }
+                return super.visitAssignment(tree, param);
+            }
 
-                @Override
-                public Void visitVariable(VariableTree tree, Void param) {
-                    final Name name = tree.getName();
-                    final ExpressionTree init = tree.getInitializer();
-                    if (Objects.equal(argName, name)
-                            && init instanceof NewClassTree) {
-                        last = parseIntentAction((NewClassTree) init);
-                    }
-                    return super.visitVariable(tree, param);
+            @Override
+            public Void visitVariable(VariableTree tree, Void param) {
+                final Name name = tree.getName();
+                final ExpressionTree init = tree.getInitializer();
+                if (Objects.equal(argName, name) && init instanceof NewClassTree) {
+                    mLast = parseIntentAction((NewClassTree) init);
                 }
-            }, null);
-            return res.get();
-        }
-        return null;
+                return super.visitVariable(tree, param);
+            }
+        }, null);
+        return res.get();
     }
 
     private static ParsedRequiresPermission parseBroadcastTargetRequiresPermission(
             MethodInvocationTree tree, VisitorState state) {
-        final ExpressionTree arg = findArgumentByParameterName(tree,
-                (name) -> name.toLowerCase().contains("permission"));
         final ParsedRequiresPermission res = new ParsedRequiresPermission();
-        if (arg != null) {
-            arg.accept(new TreeScanner<Void, Void>() {
-                @Override
-                public Void visitIdentifier(IdentifierTree tree, Void param) {
-                    res.addConstValue(tree);
-                    return super.visitIdentifier(tree, param);
-                }
-
-                @Override
-                public Void visitMemberSelect(MemberSelectTree tree, Void param) {
-                    res.addConstValue(tree);
-                    return super.visitMemberSelect(tree, param);
-                }
-            }, null);
+        int permission_position = 1;
+        if (SEND_BROADCAST_AS_USER.matches(tree, state)) {
+            permission_position = 2;
         }
+        if (tree.getArguments().size() < permission_position + 1) {
+            return res;
+        }
+        final ExpressionTree arg = tree.getArguments().get(permission_position);
+        arg.accept(new TreeScanner<Void, Void>() {
+            @Override
+            public Void visitIdentifier(IdentifierTree tree, Void param) {
+                res.addConstValue(tree);
+                return super.visitIdentifier(tree, param);
+            }
+
+            @Override
+            public Void visitMemberSelect(MemberSelectTree tree, Void param) {
+                res.addConstValue(tree);
+                return super.visitMemberSelect(tree, param);
+            }
+        }, null);
         return res;
     }
 
diff --git a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
index e53372d..05fde7c 100644
--- a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
+++ b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
@@ -412,6 +412,19 @@
                         "      context.sendBroadcast(intent);",
                         "    }",
                         "  }",
+                        "  public void exampleWithChainedMethod(Context context) {",
+                        "    Intent intent = new Intent(FooManager.ACTION_RED)",
+                        "            .putExtra(\"foo\", 42);",
+                        "    context.sendBroadcast(intent, FooManager.PERMISSION_RED);",
+                        "    context.sendBroadcastWithMultiplePermissions(intent,",
+                        "        new String[] { FooManager.PERMISSION_RED });",
+                        "  }",
+                        "  public void exampleWithAsUser(Context context) {",
+                        "    Intent intent = new Intent(FooManager.ACTION_RED);",
+                        "    context.sendBroadcastAsUser(intent, 42, FooManager.PERMISSION_RED);",
+                        "    context.sendBroadcastAsUserMultiplePermissions(intent, 42,",
+                        "        new String[] { FooManager.PERMISSION_RED });",
+                        "  }",
                         "}")
                 .doTest();
     }
diff --git a/errorprone/tests/res/android/content/Context.java b/errorprone/tests/res/android/content/Context.java
index efc4fb1..9d622ff 100644
--- a/errorprone/tests/res/android/content/Context.java
+++ b/errorprone/tests/res/android/content/Context.java
@@ -36,4 +36,15 @@
     public void sendBroadcastWithMultiplePermissions(Intent intent, String[] receiverPermissions) {
         throw new UnsupportedOperationException();
     }
+
+    /* Fake user type for test purposes */
+    public void sendBroadcastAsUser(Intent intent, int user, String receiverPermission) {
+        throw new UnsupportedOperationException();
+    }
+
+    /* Fake user type for test purposes */
+    public void sendBroadcastAsUserMultiplePermissions(
+            Intent intent, int user, String[] receiverPermissions) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/errorprone/tests/res/android/content/Intent.java b/errorprone/tests/res/android/content/Intent.java
index 288396e..7ccea78 100644
--- a/errorprone/tests/res/android/content/Intent.java
+++ b/errorprone/tests/res/android/content/Intent.java
@@ -24,4 +24,8 @@
     public Intent setAction(String action) {
         throw new UnsupportedOperationException();
     }
+
+    public Intent putExtra(String extra, int value) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index e6cb3a0..5135e9e 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -239,6 +239,9 @@
         "wmshell.protolog.json.gz",
         "wmshell.protolog.pb",
     ],
+    flags_packages: [
+        "com_android_wm_shell_flags",
+    ],
     kotlincflags: ["-Xjvm-default=all"],
     manifest: "AndroidManifest.xml",
     plugins: ["dagger2-compiler"],
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 52ae93f..bbbc23e 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -34,6 +34,7 @@
 
         <activity
             android:name=".bubbles.shortcut.CreateBubbleShortcutActivity"
+            android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles"
             android:exported="true"
             android:excludeFromRecents="true"
             android:theme="@android:style/Theme.NoDisplay"
@@ -47,6 +48,7 @@
 
         <activity
             android:name=".bubbles.shortcut.ShowBubblesActivity"
+            android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles"
             android:exported="true"
             android:excludeFromRecents="true"
             android:theme="@android:style/Theme.NoDisplay" >
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index f7a5c27..d4d9d00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.bubbles;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -225,8 +225,7 @@
                     options.setTaskAlwaysOnTop(true);
                     options.setLaunchedFromBubble(true);
                     options.setPendingIntentBackgroundActivityStartMode(
-                            MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-                    options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+                            MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
 
                     Intent fillInIntent = new Intent();
                     // Apply flags to make behaviour match documentLaunchMode=always.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index c79d9c4..5e2141a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -15,7 +15,7 @@
  */
 package com.android.wm.shell.bubbles;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -103,8 +103,7 @@
                     options.setTaskAlwaysOnTop(true);
                     options.setLaunchedFromBubble(true);
                     options.setPendingIntentBackgroundActivityStartMode(
-                            MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-                    options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+                            MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
 
                     Intent fillInIntent = new Intent();
                     // Apply flags to make behaviour match documentLaunchMode=always.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 1bf1259..da212e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -66,7 +66,7 @@
                         idealSize
                     }
                 } else {
-                    maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+                    maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
                 }
             }
             ORIENTATION_PORTRAIT -> {
@@ -85,13 +85,13 @@
                 } else {
                     if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
                         // Apply custom app width and calculate maximum size
-                        maximumSizeMaintainingAspectRatio(
+                        maximizeSizeGivenAspectRatio(
                             taskInfo,
                             Size(customPortraitWidthForLandscapeApp, idealSize.height),
                             appAspectRatio
                         )
                     } else {
-                        maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+                        maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
                     }
                 }
             }
@@ -107,7 +107,7 @@
  * Calculates the largest size that can fit in a given area while maintaining a specific aspect
  * ratio.
  */
-fun maximumSizeMaintainingAspectRatio(
+fun maximizeSizeGivenAspectRatio(
     taskInfo: RunningTaskInfo,
     targetArea: Size,
     aspectRatio: Float
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 9e6099f..a91edaa 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
@@ -671,7 +671,7 @@
             } else {
                 // if non-resizable then calculate max bounds according to aspect ratio
                 val activityAspectRatio = calculateAspectRatio(taskInfo)
-                val newSize = maximumSizeMaintainingAspectRatio(taskInfo,
+                val newSize = maximizeSizeGivenAspectRatio(taskInfo,
                     Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
                 val newBounds = centerInArea(
                     newSize, stableBounds, stableBounds.left, stableBounds.top)
@@ -818,9 +818,8 @@
         val intent = Intent(context, DesktopWallpaperActivity::class.java)
         val options =
             ActivityOptions.makeBasic().apply {
-                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
                 pendingIntentBackgroundActivityStartMode =
-                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
             }
         val pendingIntent =
             PendingIntent.getActivity(
@@ -1080,7 +1079,6 @@
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
     ) {
-        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
         val targetWindowingMode =
@@ -1090,9 +1088,6 @@
             } else {
                 WINDOWING_MODE_FREEFORM
             }
-        if (Flags.enableWindowingDynamicInitialBounds()) {
-            wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
-        }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
         wct.reorder(taskInfo.token, true /* onTop */)
         if (useDesktopOverrideDensity()) {
@@ -1426,7 +1421,6 @@
                 setPendingIntentBackgroundActivityStartMode(
                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
                 )
-                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
             }
         val wct = WindowContainerTransaction()
         wct.sendPendingIntent(launchIntent, null, opts.toBundle())
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index 95fe8b6..7e03624 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.draganddrop;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -280,8 +280,7 @@
         baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true);
         // Put BAL flags to avoid activity start aborted.
         baseActivityOpts.setPendingIntentBackgroundActivityStartMode(
-                MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-        baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+                MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
         final Bundle opts = baseActivityOpts.toBundle();
         if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) {
             opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS));
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 9bcd9b0..dbeee3b 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
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.splitscreen;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
@@ -1909,8 +1909,8 @@
         }
         // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
         // will be canceled.
-        options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-        options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+        options.setPendingIntentBackgroundActivityStartMode(
+                MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
 
         // TODO (b/336477473): Disallow enter PiP when launching a task in split by default;
         //                     this might have to be changed as more split-to-pip cujs are defined.
@@ -3739,8 +3739,9 @@
                 mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType,
                         EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
                 Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow",
-                        "app package " + taskInfo.baseActivity.getPackageName()
-                        + " does not support splitscreen, or is a controlled activity type"));
+                        "app package " + taskInfo.baseIntent.getComponent()
+                                + " does not support splitscreen, or is a controlled activity"
+                                + " type"));
                 if (splitScreenVisible) {
                     handleUnsupportedSplitStart();
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
index dd4595a..287e779 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
@@ -48,6 +48,7 @@
 
     public ShellInit(ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
+        ProtoLog.registerGroups(ShellProtoLogGroup.values());
     }
 
     /**
@@ -76,7 +77,6 @@
      */
     @VisibleForTesting
     public void init() {
-        ProtoLog.registerGroups(ShellProtoLogGroup.values());
         ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size());
         SurfaceControl.setDebugUsageAfterRelease(true);
         // Init in order of registration
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
index db962e7..2406bde 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
@@ -48,7 +48,10 @@
 
     @Before
     fun setup() {
-        tapl.workspace.switchToOverview().dismissAllTasks()
+        val overview = tapl.workspace.switchToOverview()
+        if (overview.hasTasks()) {
+            overview.dismissAllTasks()
+        }
 
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
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 6002c21..8421365 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
@@ -580,138 +580,6 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
-    val task = setUpFullscreenTask()
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
-    val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
-    val task =
-        setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
-            shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
-    val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
   fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
     val task = setUpFullscreenTask()
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index d18fec2..e7b4c50 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -347,8 +347,7 @@
 
         assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token);
         assertThat(options.getPendingIntentBackgroundActivityStartMode())
-                .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-        assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue();
+                .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
     }
 
     @Test
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 2d0e7ab..a255f73 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2651,4 +2651,11 @@
      * @hide
      */
     public static native boolean isBluetoothVariableLatencyEnabled();
+
+    /**
+     * Register a native listener for system property sysprop
+     * @param callback the listener which fires when the property changes
+     * @hide
+     */
+    public static native void listenForSystemPropertyChange(String sysprop, Runnable callback);
 }
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 999f40e5..1c5049e 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -23,6 +23,7 @@
 import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.VirtualDisplayFlag;
 import android.hardware.display.VirtualDisplay;
 import android.hardware.display.VirtualDisplayConfig;
 import android.os.Build;
@@ -140,6 +141,7 @@
     /**
      * @hide
      */
+    @Nullable
     public VirtualDisplay createVirtualDisplay(@NonNull String name,
             int width, int height, int dpi, boolean isSecure, @Nullable Surface surface,
             @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
@@ -192,6 +194,11 @@
      *                                 <li>If attempting to create a new virtual display
      *                                 associated with this MediaProjection instance after it has
      *                                 been stopped by invoking {@link #stop()}.
+     *                                 <li>If attempting to create a new virtual display
+     *                                 associated with this MediaProjection instance after a
+     *                                 {@link MediaProjection.Callback#onStop()} callback has been
+     *                                 received due to the user or the system stopping the
+     *                                 MediaProjection session.
      *                                 <li>If the target SDK is {@link
      *                                 android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up,
      *                                 and if this instance has already taken a recording through
@@ -208,12 +215,17 @@
      *                               {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}.
      *                               Instead, recording doesn't begin until the user re-grants
      *                               consent in the dialog.
+     * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay}
+     * could be created.
      * @see VirtualDisplay
      * @see VirtualDisplay.Callback
      */
+    @SuppressWarnings("RequiresPermission")
+    @Nullable
     public VirtualDisplay createVirtualDisplay(@NonNull String name,
-            int width, int height, int dpi, int flags, @Nullable Surface surface,
-            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
+            int width, int height, int dpi, @VirtualDisplayFlag int flags,
+            @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback,
+            @Nullable Handler handler) {
         if (shouldMediaProjectionRequireCallback()) {
             if (mCallbacks.isEmpty()) {
                 final IllegalStateException e = new IllegalStateException(
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index 88770d4..186b69b 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -912,8 +912,10 @@
                     "message: $message"
             )
         }
+
+        val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)
+
         if (statusCode == PackageInstaller.STATUS_SUCCESS) {
-            val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)
             val resultIntent = if (shouldReturnResult) {
                 Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED)
             } else {
@@ -922,12 +924,34 @@
             }
             _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent))
         } else {
-            if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) {
+            // TODO (b/346655018): Use INSTALL_FAILED_ABORTED legacyCode in the condition
+            // statusCode can be STATUS_FAILURE_ABORTED if:
+            // 1. GPP blocks an install.
+            // 2. User denies ownership update explicitly.
+            // InstallFailed dialog must not be shown only when the user denies ownership update. We
+            // must show this dialog for all other install failures.
+
+            val userDenied =
+                    statusCode == PackageInstaller.STATUS_FAILURE_ABORTED &&
+                    legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_TIMEOUT &&
+                    legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE
+
+            if (shouldReturnResult) {
+                val resultIntent = Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus)
                 _installResult.setValue(
-                    InstallFailed(appSnippet, statusCode, legacyStatus, message)
+                    InstallFailed(
+                        legacyCode = legacyStatus,
+                        statusCode = statusCode,
+                        shouldReturnResult = true,
+                        resultIntent = resultIntent
+                    )
                 )
-            } else {
+            } else if (userDenied) {
                 _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR))
+            } else {
+                _installResult.setValue(
+                    InstallFailed(appSnippet, legacyStatus, statusCode, message)
+                )
             }
         }
     }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
index 5dd4d29..8de8fbb 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.content.pm.PackageInstaller
 import android.graphics.drawable.Drawable
 
 sealed class InstallStage(val stageCode: Int) {
@@ -77,11 +78,10 @@
     val shouldReturnResult: Boolean = false,
     /**
      *
-     * * If the caller is requesting a result back, this will hold the Intent with
-     * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED] which is sent
-     * back to the caller.
+     * * If the caller is requesting a result back, this will hold an Intent with
+     * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED].
      *
-     * * If the caller doesn't want the result back, this will hold the Intent that launches
+     * * If the caller doesn't want the result back, this will hold an Intent that launches
      * the newly installed / updated app if a launchable activity exists.
      */
     val resultIntent: Intent? = null,
@@ -95,17 +95,23 @@
 }
 
 data class InstallFailed(
-    private val appSnippet: PackageUtil.AppSnippet,
+    private val appSnippet: PackageUtil.AppSnippet? = null,
     val legacyCode: Int,
     val statusCode: Int,
-    val message: String?,
+    val message: String? = null,
+    val shouldReturnResult: Boolean = false,
+    /**
+     * If the caller is requesting a result back, this will hold an Intent with
+     * [Intent.EXTRA_INSTALL_RESULT] set to the [PackageInstaller.EXTRA_LEGACY_STATUS].
+     */
+    val resultIntent: Intent? = null
 ) : InstallStage(STAGE_FAILED) {
 
     val appIcon: Drawable?
-        get() = appSnippet.icon
+        get() = appSnippet?.icon
 
     val appLabel: String?
-        get() = appSnippet.label as String?
+        get() = appSnippet?.label as String?
 }
 
 data class InstallAborted(
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
index 31b9ccb..e2ab316 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
@@ -171,15 +171,20 @@
                     val successIntent = success.resultIntent
                     setResult(Activity.RESULT_OK, successIntent, true)
                 } else {
-                    val successFragment = InstallSuccessFragment(success)
-                    showDialogInner(successFragment)
+                    val successDialog = InstallSuccessFragment(success)
+                    showDialogInner(successDialog)
                 }
             }
 
             InstallStage.STAGE_FAILED -> {
                 val failed = installStage as InstallFailed
-                val failedDialog = InstallFailedFragment(failed)
-                showDialogInner(failedDialog)
+                if (failed.shouldReturnResult) {
+                    val failureIntent = failed.resultIntent
+                    setResult(Activity.RESULT_FIRST_USER, failureIntent, true)
+                } else {
+                    val failureDialog = InstallFailedFragment(failed)
+                    showDialogInner(failureDialog)
+                }
             }
 
             else -> {
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
index 4028b73..714f951 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
@@ -18,9 +18,7 @@
 
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
-import android.telephony.UiccCardInfo;
 import android.telephony.UiccPortInfo;
-import android.telephony.UiccSlotInfo;
 import android.telephony.UiccSlotMapping;
 
 public class DataServiceUtils {
@@ -71,53 +69,9 @@
         public static final String COLUMN_ID = "sudId";
 
         /**
-         * The name of the physical slot index column, see
-         * {@link UiccSlotMapping#getPhysicalSlotIndex()}.
-         */
-        public static final String COLUMN_PHYSICAL_SLOT_INDEX = "physicalSlotIndex";
-
-        /**
-         * The name of the logical slot index column, see
-         * {@link UiccSlotMapping#getLogicalSlotIndex()}.
-         */
-        public static final String COLUMN_LOGICAL_SLOT_INDEX = "logicalSlotIndex";
-
-        /**
-         * The name of the card ID column, see {@link UiccCardInfo#getCardId()}.
-         */
-        public static final String COLUMN_CARD_ID = "cardId";
-
-        /**
-         * The name of the eUICC state column, see {@link UiccCardInfo#isEuicc()}.
-         */
-        public static final String COLUMN_IS_EUICC = "isEuicc";
-
-        /**
-         * The name of the multiple enabled profiles supported state column, see
-         * {@link UiccCardInfo#isMultipleEnabledProfilesSupported()}.
-         */
-        public static final String COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED =
-                "isMultipleEnabledProfilesSupported";
-
-        /**
-         * The name of the card state column, see {@link UiccSlotInfo#getCardStateInfo()}.
-         */
-        public static final String COLUMN_CARD_STATE = "cardState";
-
-        /**
-         * The name of the removable state column, see {@link UiccSlotInfo#isRemovable()}.
-         */
-        public static final String COLUMN_IS_REMOVABLE = "isRemovable";
-
-        /**
          * The name of the active state column, see {@link UiccPortInfo#isActive()}.
          */
         public static final String COLUMN_IS_ACTIVE = "isActive";
-
-        /**
-         * The name of the port index column, see {@link UiccPortInfo#getPortIndex()}.
-         */
-        public static final String COLUMN_PORT_INDEX = "portIndex";
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
index c92204f..5f7fa27 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
@@ -19,14 +19,13 @@
 import android.content.Context;
 import android.util.Log;
 
-import java.util.List;
-import java.util.Objects;
-
 import androidx.lifecycle.LiveData;
 import androidx.room.Database;
 import androidx.room.Room;
 import androidx.room.RoomDatabase;
-import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import java.util.List;
+import java.util.Objects;
 
 @Database(entities = {SubscriptionInfoEntity.class, UiccInfoEntity.class,
         MobileNetworkInfoEntity.class}, exportSchema = false, version = 1)
@@ -132,13 +131,6 @@
     }
 
     /**
-     * Query the UICC info by the subscription ID from the UiccInfoEntity table.
-     */
-    public LiveData<UiccInfoEntity> queryUiccInfoById(String id) {
-        return mUiccInfoDao().queryUiccInfoById(id);
-    }
-
-    /**
      * Delete the subscriptionInfo info by the subscription ID from the SubscriptionInfoEntity
      * table.
      */
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java
index 7e60421..90e5189 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java
@@ -16,14 +16,14 @@
 
 package com.android.settingslib.mobile.dataservice;
 
-import java.util.List;
-
 import androidx.lifecycle.LiveData;
 import androidx.room.Dao;
 import androidx.room.Insert;
 import androidx.room.OnConflictStrategy;
 import androidx.room.Query;
 
+import java.util.List;
+
 @Dao
 public interface UiccInfoDao {
 
@@ -34,14 +34,6 @@
             + DataServiceUtils.UiccInfoData.COLUMN_ID)
     LiveData<List<UiccInfoEntity>> queryAllUiccInfos();
 
-    @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE "
-            + DataServiceUtils.UiccInfoData.COLUMN_ID + " = :subId")
-    LiveData<UiccInfoEntity> queryUiccInfoById(String subId);
-
-    @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE "
-            + DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC + " = :isEuicc")
-    LiveData<List<UiccInfoEntity>> queryUiccInfosByEuicc(boolean isEuicc);
-
     @Query("SELECT COUNT(*) FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME)
     int count();
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java
index 2ccf295..0f80edf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java
@@ -26,20 +26,9 @@
 @Entity(tableName = DataServiceUtils.UiccInfoData.TABLE_NAME)
 public class UiccInfoEntity {
 
-    public UiccInfoEntity(@NonNull String subId, @NonNull String physicalSlotIndex,
-            int logicalSlotIndex, int cardId, boolean isEuicc,
-            boolean isMultipleEnabledProfilesSupported, int cardState, boolean isRemovable,
-            boolean isActive, int portIndex) {
+    public UiccInfoEntity(@NonNull String subId, boolean isActive) {
         this.subId = subId;
-        this.physicalSlotIndex = physicalSlotIndex;
-        this.logicalSlotIndex = logicalSlotIndex;
-        this.cardId = cardId;
-        this.isEuicc = isEuicc;
-        this.isMultipleEnabledProfilesSupported = isMultipleEnabledProfilesSupported;
-        this.cardState = cardState;
-        this.isRemovable = isRemovable;
         this.isActive = isActive;
-        this.portIndex = portIndex;
     }
 
     @PrimaryKey
@@ -47,48 +36,14 @@
     @NonNull
     public String subId;
 
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PHYSICAL_SLOT_INDEX)
-    @NonNull
-    public String physicalSlotIndex;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_LOGICAL_SLOT_INDEX)
-    public int logicalSlotIndex;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_ID)
-    public int cardId;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC)
-    public boolean isEuicc;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED)
-    public boolean isMultipleEnabledProfilesSupported;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_STATE)
-    public int cardState;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_REMOVABLE)
-    public boolean isRemovable;
-
     @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_ACTIVE)
     public boolean isActive;
 
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PORT_INDEX)
-    public int portIndex;
-
-
     @Override
     public int hashCode() {
         int result = 17;
         result = 31 * result + subId.hashCode();
-        result = 31 * result + physicalSlotIndex.hashCode();
-        result = 31 * result + logicalSlotIndex;
-        result = 31 * result + cardId;
-        result = 31 * result + Boolean.hashCode(isEuicc);
-        result = 31 * result + Boolean.hashCode(isMultipleEnabledProfilesSupported);
-        result = 31 * result + cardState;
-        result = 31 * result + Boolean.hashCode(isRemovable);
         result = 31 * result + Boolean.hashCode(isActive);
-        result = 31 * result + portIndex;
         return result;
     }
 
@@ -102,40 +57,15 @@
         }
 
         UiccInfoEntity info = (UiccInfoEntity) obj;
-        return  TextUtils.equals(subId, info.subId)
-                && TextUtils.equals(physicalSlotIndex, info.physicalSlotIndex)
-                && logicalSlotIndex == info.logicalSlotIndex
-                && cardId == info.cardId
-                && isEuicc == info.isEuicc
-                && isMultipleEnabledProfilesSupported == info.isMultipleEnabledProfilesSupported
-                && cardState == info.cardState
-                && isRemovable == info.isRemovable
-                && isActive == info.isActive
-                && portIndex == info.portIndex;
+        return TextUtils.equals(subId, info.subId) && isActive == info.isActive;
     }
 
     public String toString() {
         StringBuilder builder = new StringBuilder();
         builder.append(" {UiccInfoEntity(subId = ")
                 .append(subId)
-                .append(", logicalSlotIndex = ")
-                .append(physicalSlotIndex)
-                .append(", logicalSlotIndex = ")
-                .append(logicalSlotIndex)
-                .append(", cardId = ")
-                .append(cardId)
-                .append(", isEuicc = ")
-                .append(isEuicc)
-                .append(", isMultipleEnabledProfilesSupported = ")
-                .append(isMultipleEnabledProfilesSupported)
-                .append(", cardState = ")
-                .append(cardState)
-                .append(", isRemovable = ")
-                .append(isRemovable)
                 .append(", isActive = ")
                 .append(isActive)
-                .append(", portIndex = ")
-                .append(portIndex)
                 .append(")}");
         return builder.toString();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
index 7886e85..49b974f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
@@ -20,6 +20,7 @@
 import android.provider.Settings
 import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.settingslib.notification.modes.ZenMode
+import java.time.Duration
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -35,8 +36,7 @@
     override val globalZenMode: StateFlow<Int>
         get() = mutableZenMode.asStateFlow()
 
-    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> =
-        MutableStateFlow(listOf(TestModeBuilder.EXAMPLE))
+    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf())
     override val modes: Flow<List<ZenMode>>
         get() = mutableModesFlow.asStateFlow()
 
@@ -52,6 +52,10 @@
         mutableZenMode.value = zenMode
     }
 
+    fun addModes(zenModes: List<ZenMode>) {
+        mutableModesFlow.value += zenModes
+    }
+
     fun addMode(id: String, active: Boolean = false) {
         mutableModesFlow.value += newMode(id, active)
     }
@@ -60,6 +64,20 @@
         mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id }
     }
 
+    override fun activateMode(zenMode: ZenMode, duration: Duration?) {
+        activateMode(zenMode.id)
+    }
+
+    override fun deactivateMode(zenMode: ZenMode) {
+        deactivateMode(zenMode.id)
+    }
+
+    fun activateMode(id: String) {
+        val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
+        removeMode(id)
+        mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build()
+    }
+
     fun deactivateMode(id: String) {
         val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
         removeMode(id)
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index b2fcb5f..0ff7f84 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -30,6 +30,7 @@
 import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.settingslib.notification.modes.ZenModesBackend
+import java.time.Duration
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
@@ -57,6 +58,10 @@
 
     /** A list of all existing priority modes. */
     val modes: Flow<List<ZenMode>>
+
+    fun activateMode(zenMode: ZenMode, duration: Duration? = null)
+
+    fun deactivateMode(zenMode: ZenMode)
 }
 
 @SuppressLint("SharedFlowCreation")
@@ -178,4 +183,12 @@
             flowOf(emptyList())
         }
     }
+
+    override fun activateMode(zenMode: ZenMode, duration: Duration?) {
+        backend.activateMode(zenMode, duration)
+    }
+
+    override fun deactivateMode(zenMode: ZenMode) {
+        backend.deactivateMode(zenMode)
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 7b994d5..2f7cdd6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -37,6 +37,13 @@
     private ZenModeConfig.ZenRule mConfigZenRule;
 
     public static final ZenMode EXAMPLE = new TestModeBuilder().build();
+    public static final ZenMode MANUAL_DND = ZenMode.manualDndMode(
+            new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd"))
+                    .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+                    .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+                    .build(),
+            true /* isActive */
+    );
 
     public TestModeBuilder() {
         // Reasonable defaults
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index a17076b..4e01a71 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -539,6 +539,7 @@
         "androidx.preference_preference",
         "androidx.appcompat_appcompat",
         "androidx.concurrent_concurrent-futures",
+        "androidx.concurrent_concurrent-futures-ktx",
         "androidx.mediarouter_mediarouter",
         "androidx.palette_palette",
         "androidx.legacy_legacy-preference-v14",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 97a45fb..7032c73 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -870,6 +870,13 @@
 }
 
 flag {
+    name: "qs_ui_refactor_compose_fragment"
+    namespace: "systemui"
+    description: "Uses a different QS fragment in NPVC that uses the new compose UI and recommended architecture. This flag depends on qs_ui_refactor flag."
+    bug: "325099249"
+}
+
+flag {
   name: "remove_dream_overlay_hide_on_touch"
   namespace: "systemui"
   description: "Removes logic to hide the dream overlay on user interaction, as it conflicts with various transitions"
@@ -1189,6 +1196,16 @@
 }
 
 flag {
+   namespace: "systemui"
+   name: "remove_update_listener_in_qs_icon_view_impl"
+   description: "Remove update listeners in QsIconViewImpl class to avoid memory leak."
+   bug: "327078684"
+   metadata {
+       purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
    name: "sim_pin_race_condition_on_restart"
    namespace: "systemui"
    description: "The SIM PIN screen may be shown incorrectly on reboot"
@@ -1206,4 +1223,4 @@
    metadata {
         purpose: PURPOSE_BUGFIX
    }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 768e653..1c02d3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -969,6 +969,8 @@
     val clickActionLabel = stringResource(R.string.accessibility_action_label_select_widget)
     val removeWidgetActionLabel = stringResource(R.string.accessibility_action_label_remove_widget)
     val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
+    val unselectWidgetActionLabel =
+        stringResource(R.string.accessibility_action_label_unselect_widget)
     val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
     val selectedIndex =
         selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
@@ -1009,18 +1011,7 @@
                                 contentListState.onSaveList()
                                 true
                             }
-                        val selectWidgetAction =
-                            CustomAccessibilityAction(clickActionLabel) {
-                                val currentWidgetKey =
-                                    index?.let {
-                                        keyAtIndexIfEditable(contentListState.list, index)
-                                    }
-                                viewModel.setSelectedKey(currentWidgetKey)
-                                true
-                            }
-
-                        val actions = mutableListOf(selectWidgetAction, deleteAction)
-
+                        val actions = mutableListOf(deleteAction)
                         if (selectedIndex != null && selectedIndex != index) {
                             actions.add(
                                 CustomAccessibilityAction(placeWidgetActionLabel) {
@@ -1032,6 +1023,21 @@
                             )
                         }
 
+                        if (!selected) {
+                            actions.add(
+                                CustomAccessibilityAction(clickActionLabel) {
+                                    viewModel.setSelectedKey(model.key)
+                                    true
+                                }
+                            )
+                        } else {
+                            actions.add(
+                                CustomAccessibilityAction(unselectWidgetActionLabel) {
+                                    viewModel.setSelectedKey(null)
+                                    true
+                                }
+                            )
+                        }
                         customActions = actions
                     }
                 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
index c34fb38..c993855 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
@@ -55,7 +55,7 @@
             modifier
                 .pointerInput(isEnabled) {
                     if (isEnabled) {
-                        detectLongPressGesture { viewModel.onLongPress() }
+                        detectLongPressGesture { viewModel.onLongPress(isA11yAction = false) }
                     }
                 }
                 .pointerInput(Unit) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index d629eec..f8bd633 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.util.animation.MeasurementInput
 
-private object MediaCarousel {
+object MediaCarousel {
     object Elements {
         internal val Content =
             ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
index 0398133..a22bc34 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
@@ -25,7 +25,7 @@
 /** [ElementScenePicker] implementation for the media carousel object. */
 object MediaScenePicker : ElementScenePicker {
 
-    private val shadeLockscreenFraction = 0.65f
+    const val SHADE_FRACTION = 0.66f
     private val scenes =
         setOf(
             Scenes.Lockscreen,
@@ -44,7 +44,7 @@
         return when {
             // TODO: 352052894 - update with the actual scene picking
             transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> {
-                if (transition.progress < shadeLockscreenFraction) {
+                if (transition.progress < SHADE_FRACTION) {
                     Scenes.Lockscreen
                 } else {
                     Scenes.Shade
@@ -53,7 +53,7 @@
 
             // TODO: 345467290 - update with the actual scene picking
             transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> {
-                if (transition.progress < 1f - shadeLockscreenFraction) {
+                if (transition.progress < 1f - SHADE_FRACTION) {
                     Scenes.Shade
                 } else {
                     Scenes.Lockscreen
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
index df47cba..7d46c75 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -25,6 +25,8 @@
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
 import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.media.controls.ui.composable.MediaCarousel
+import com.android.systemui.media.controls.ui.composable.MediaScenePicker
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.scene.shared.model.Scenes
@@ -59,10 +61,13 @@
         fade(QuickSettings.Elements.SplitShadeQuickSettings)
         fade(QuickSettings.Elements.FooterActions)
     }
-    translate(
-        QuickSettings.Elements.QuickQuickSettings,
-        y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
-    )
+
+    val qsTranslation = ShadeHeader.Dimensions.CollapsedHeight * MediaScenePicker.SHADE_FRACTION
+    val qsExpansionDiff =
+        ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight
+
+    translate(QuickSettings.Elements.QuickQuickSettings, y = -qsTranslation)
+    translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation))
     translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
 }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
index a8bdc7c..1f5e30c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
@@ -88,24 +88,6 @@
         }
 
     @Test
-    fun isDisclaimerDismissed_byDefault_isFalse() =
-        testScope.runTest {
-            val isDisclaimerDismissed by
-                collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER))
-            assertThat(isDisclaimerDismissed).isFalse()
-        }
-
-    @Test
-    fun isDisclaimerDismissed_onSet_isTrue() =
-        testScope.runTest {
-            val isDisclaimerDismissed by
-                collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER))
-
-            underTest.setDisclaimerDismissed(MAIN_USER)
-            assertThat(isDisclaimerDismissed).isTrue()
-        }
-
-    @Test
     fun getSharedPreferences_whenFileRestored() =
         testScope.runTest {
             val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 5cdbe9c..9539c04 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -84,6 +84,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -1059,6 +1060,25 @@
         )
     }
 
+    @Test
+    fun dismissDisclaimerSetsDismissedFlag() =
+        testScope.runTest {
+            val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
+            assertThat(disclaimerDismissed).isFalse()
+            underTest.setDisclaimerDismissed()
+            assertThat(disclaimerDismissed).isTrue()
+        }
+
+    @Test
+    fun dismissDisclaimerTimeoutResetsDismissedFlag() =
+        testScope.runTest {
+            val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
+            underTest.setDisclaimerDismissed()
+            assertThat(disclaimerDismissed).isTrue()
+            advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS)
+            assertThat(disclaimerDismissed).isFalse()
+        }
+
     private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) {
         whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id)))
             .thenReturn(disabledFlags)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt
index 7b79d28..9a92f76 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt
@@ -74,40 +74,6 @@
             assertThat(isCtaDismissed).isFalse()
         }
 
-    @Test
-    fun setDisclaimerDismissed_currentUser() =
-        testScope.runTest {
-            setSelectedUser(MAIN_USER)
-            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
-
-            assertThat(isDisclaimerDismissed).isFalse()
-            underTest.setDisclaimerDismissed(MAIN_USER)
-            assertThat(isDisclaimerDismissed).isTrue()
-        }
-
-    @Test
-    fun setDisclaimerDismissed_anotherUser() =
-        testScope.runTest {
-            setSelectedUser(MAIN_USER)
-            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
-
-            assertThat(isDisclaimerDismissed).isFalse()
-            underTest.setDisclaimerDismissed(SECONDARY_USER)
-            assertThat(isDisclaimerDismissed).isFalse()
-        }
-
-    @Test
-    fun isDisclaimerDismissed_userSwitch() =
-        testScope.runTest {
-            setSelectedUser(MAIN_USER)
-            underTest.setDisclaimerDismissed(MAIN_USER)
-            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
-
-            assertThat(isDisclaimerDismissed).isTrue()
-            setSelectedUser(SECONDARY_USER)
-            assertThat(isDisclaimerDismissed).isFalse()
-        }
-
     private suspend fun setSelectedUser(user: UserInfo) {
         with(kosmos.fakeUserRepository) {
             setUserInfos(listOf(user))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index b138fb3..f8906ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -65,6 +65,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -352,6 +353,21 @@
         }
 
     @Test
+    fun showDisclaimer_trueWhenTimeout() =
+        testScope.runTest {
+            underTest.setEditModeState(EditModeState.SHOWING)
+            kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+
+            val showDisclaimer by collectLastValue(underTest.showDisclaimer)
+
+            assertThat(showDisclaimer).isTrue()
+            underTest.onDisclaimerDismissed()
+            assertThat(showDisclaimer).isFalse()
+            advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS)
+            assertThat(showDisclaimer).isTrue()
+        }
+
+    @Test
     fun scrollPosition_persistedOnEditCleanup() {
         val index = 2
         val offset = 30
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
new file mode 100644
index 0000000..ee51e37
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.systemui.education.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.education.GestureType.BACK_GESTURE
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.keyboardTouchpadEduStatsInteractor
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCount() =
+        testScope.runTest {
+            val model by
+                collectLastValue(
+                    kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
+                )
+            val originalValue = model!!.signalCount
+            underTest.incrementSignalCount(BACK_GESTURE)
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataAddedOnUpdateShortcutTriggerTime() =
+        testScope.runTest {
+            val model by
+                collectLastValue(
+                    kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
+                )
+            assertThat(model?.lastShortcutTriggeredTime).isNull()
+            underTest.updateShortcutTriggerTime(BACK_GESTURE)
+            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index 96b4b43..81db16b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -185,6 +185,21 @@
         }
 
     @Test
+    fun longPressed_isA11yAction_doesNotShowMenu_opensSettings() =
+        testScope.runTest {
+            createUnderTest(isOpenWppDirectlyEnabled = true)
+            val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+            val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
+            val isA11yAction = true
+            runCurrent()
+
+            underTest.onLongPress(isA11yAction)
+
+            assertThat(isMenuVisible).isFalse()
+            assertThat(shouldOpenSettings).isTrue()
+        }
+
+    @Test
     fun longPressed_closeDialogsBroadcastReceived_popupDismissed() =
         testScope.runTest {
             val isMenuVisible by collectLastValue(underTest.isMenuVisible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 278c90a..fd2e335 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -159,8 +159,9 @@
             )
 
             assertThat(values[0]).isEqualTo(1f)
-            // Should fade to zero between here
             assertThat(values[1]).isEqualTo(0f)
+            // Should always finish with 1f to show HUNs
+            assertThat(values[2]).isEqualTo(1f)
         }
 
     @Test
@@ -177,7 +178,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(2)
+            assertThat(values.size).isEqualTo(3)
             // Shade stays open, and alpha should remain visible
             values.forEach { assertThat(it).isEqualTo(1f) }
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index a120bdc..540a85a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -175,12 +175,14 @@
             transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
             assertThat(isVisible).isFalse()
 
-            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+            kosmos.headsUpNotificationRepository.setNotifications(
                 buildNotificationRows(isPinned = true)
+            )
             assertThat(isVisible).isTrue()
 
-            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+            kosmos.headsUpNotificationRepository.setNotifications(
                 buildNotificationRows(isPinned = false)
+            )
             assertThat(isVisible).isFalse()
         }
 
@@ -1699,8 +1701,8 @@
         return transitionStateFlow
     }
 
-    private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
-        setOf(
+    private fun buildNotificationRows(isPinned: Boolean = false): List<HeadsUpRowRepository> =
+        listOf(
             fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
             fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
             fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
new file mode 100644
index 0000000..8810ade
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -0,0 +1,510 @@
+/*
+ * 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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.app.Notification
+import android.app.NotificationManager.IMPORTANCE_DEFAULT
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.modifyEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.StringSubject
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply {
+            testDispatcher = UnconfinedTestDispatcher()
+            statusBarStateController =
+                mock<SysuiStatusBarStateController>().also { mock ->
+                    doAnswer { statusBarState.ordinal }.whenever(mock).state
+                }
+            fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+        }
+    private val notifPipeline: NotifPipeline = mock()
+    private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
+
+    @Test
+    fun topUnseenSectioner() {
+        val solo = NotificationEntryBuilder().setTag("solo").build()
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child1.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = parent.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+        }
+    }
+
+    @Test
+    fun topOngoingSectioner() {
+        val solo = NotificationEntryBuilder().setTag("solo").build()
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = parent.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+        }
+    }
+
+    @Test
+    fun testPromoter() {
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val child3 = NotificationEntryBuilder().setTag("child3").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        GroupEntryBuilder()
+            .addChild(child1)
+            .addChild(child2)
+            .addChild(child3)
+            .setSummary(parent)
+            .build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+            assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child2))
+                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+            assertThat(promoter.shouldPromoteToTopLevel(child2))
+                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+        }
+    }
+
+    @Test
+    fun topOngoingIdentifier() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val parent = defaultEntryBuilder().setTag("parent").setRank(3).build()
+        val child1 = defaultEntryBuilder().setTag("child1").setRank(4).build()
+        val child2 = defaultEntryBuilder().setTag("child2").setRank(5).build()
+        val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+        val listEntryList = listOf(group, solo1, solo2)
+
+        runCoordinatorTest {
+            // TEST: base case - no entries in the list
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: none of these are unseen or ongoing yet, so don't pick them
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: when solo2 is the only one colorized, it gets picked up
+            solo2.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: once solo1 is colorized, it takes priority for being ranked higher
+            solo1.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: changing just the rank of solo1 causes it to pick up solo2 instead
+            solo1.modifyEntry { setRank(20) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching to SHADE disables the whole thing
+            statusBarState = StatusBarState.SHADE
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching back to KEYGUARD picks up the same entry again
+            statusBarState = StatusBarState.KEYGUARD
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: updating to not colorized revokes the top-ongoing status
+            solo2.setColorizedFgs(false)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: updating the importance to LOW revokes top-ongoing status
+            solo1.modifyEntry { setImportance(IMPORTANCE_LOW) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun topUnseenIdentifier() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val parent = defaultEntryBuilder().setTag("parent").setRank(4).build()
+        val child1 = defaultEntryBuilder().setTag("child1").setRank(5).build()
+        val child2 = defaultEntryBuilder().setTag("child2").setRank(6).build()
+        val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+        val listEntryList = listOf(group, solo1, solo2)
+        val notificationEntryList = listOf(solo1, solo2, parent, child1, child2)
+
+        runCoordinatorTest {
+            // All entries are added (and now unseen)
+            notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+            // TEST: Filtered out entries are ignored
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: top-ranked unseen child is selected (not the summary)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(group))
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(child1.key)
+
+            // TEST: top-ranked entry is picked
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen
+            solo1.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: no more colorized entries
+            solo1.setColorizedFgs(false)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: if the rank of solo1 is reduced, solo2 will be preferred
+            solo1.modifyEntry { setRank(3) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: switching to SHADE state will disable the entire selector
+            statusBarState = StatusBarState.SHADE
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching back to KEYGUARD re-enables the selector
+            statusBarState = StatusBarState.KEYGUARD
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: QS Expansion does not mark entries as seen
+            setShadeAndQsExpansionThenWait(0f, 1f)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: Shade expansion does mark entries as seen
+            setShadeAndQsExpansionThenWait(1f, 0f)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: Entries updated while shade is expanded are NOT marked unseen
+            collectionListener.onEntryUpdated(solo1)
+            collectionListener.onEntryUpdated(solo2)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: Entries updated after shade is collapsed ARE marked unseen
+            setShadeAndQsExpansionThenWait(0f, 0f)
+            collectionListener.onEntryUpdated(solo1)
+            collectionListener.onEntryUpdated(solo2)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: low importance disqualifies the entry for top unseen
+            solo2.modifyEntry { setImportance(IMPORTANCE_LOW) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+        }
+    }
+
+    @Test
+    fun topUnseenIdentifier_headsUpMarksSeen() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val listEntryList = listOf(solo1, solo2)
+        val notificationEntryList = listOf(solo1, solo2)
+
+        val hunRepo1 = solo1.fakeHeadsUpRowRepository()
+        val hunRepo2 = solo2.fakeHeadsUpRowRepository()
+
+        runCoordinatorTest {
+            // All entries are added (and now unseen)
+            notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+            // TEST: top-ranked entry is picked
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: heads up state and waiting isn't enough to be seen
+            kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value =
+                listOf(hunRepo1, hunRepo2)
+            testScheduler.advanceTimeBy(1.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: even being pinned doesn't take effect immediately
+            hunRepo1.isPinned.value = true
+            testScheduler.advanceTimeBy(0.5.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: after being pinned a full second, solo1 is seen
+            testScheduler.advanceTimeBy(0.5.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: repeat; being heads up and pinned for 1 second triggers seen
+            kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = listOf(hunRepo2)
+            hunRepo1.isPinned.value = false
+            hunRepo2.isPinned.value = true
+            testScheduler.advanceTimeBy(1.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(null)
+        }
+    }
+
+    private fun NotificationEntry.fakeHeadsUpRowRepository() =
+        FakeHeadsUpRowRepository(key = key, elementKey = Any())
+
+    private fun KeyguardCoordinatorTestScope.setShadeAndQsExpansionThenWait(
+        shadeExpansion: Float,
+        qsExpansion: Float
+    ) {
+        kosmos.shadeTestUtil.setShadeAndQsExpansion(shadeExpansion, qsExpansion)
+        // The coordinator waits a fraction of a second for the shade expansion to stick.
+        testScheduler.advanceTimeBy(1.seconds)
+    }
+
+    private fun defaultEntryBuilder() = NotificationEntryBuilder().setImportance(IMPORTANCE_DEFAULT)
+
+    private fun runCoordinatorTest(testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit) {
+        kosmos.lockScreenMinimalismCoordinator.attach(notifPipeline)
+        kosmos.testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
+            KeyguardCoordinatorTestScope(
+                    kosmos.lockScreenMinimalismCoordinator,
+                    kosmos.testScope,
+                    kosmos.fakeSettings,
+                )
+                .testBlock()
+        }
+    }
+
+    private inner class KeyguardCoordinatorTestScope(
+        private val coordinator: LockScreenMinimalismCoordinator,
+        private val scope: TestScope,
+        private val fakeSettings: FakeSettings,
+    ) : CoroutineScope by scope {
+        fun assertThatTopOngoingKey(): StringSubject {
+            return assertThat(
+                kosmos.activeNotificationListRepository.topOngoingNotificationKey.value
+            )
+        }
+
+        fun assertThatTopUnseenKey(): StringSubject {
+            return assertThat(
+                kosmos.activeNotificationListRepository.topUnseenNotificationKey.value
+            )
+        }
+
+        val testScheduler: TestCoroutineScheduler
+            get() = scope.testScheduler
+
+        val promoter: NotifPromoter
+            get() = coordinator.unseenNotifPromoter
+
+        val topUnseenSectioner: NotifSectioner
+            get() = coordinator.topUnseenSectioner
+
+        val topOngoingSectioner: NotifSectioner
+            get() = coordinator.topOngoingSectioner
+
+        val onBeforeTransformGroupsListener: OnBeforeTransformGroupsListener =
+            argumentCaptor { verify(notifPipeline).addOnBeforeTransformGroupsListener(capture()) }
+                .lastValue
+
+        val collectionListener: NotifCollectionListener =
+            argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
+
+        var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
+            get() =
+                fakeSettings.getIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    UserHandle.USER_CURRENT,
+                ) == 1
+            set(value) {
+                fakeSettings.putIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    if (value) 1 else 2,
+                    UserHandle.USER_CURRENT,
+                )
+            }
+    }
+
+    companion object {
+
+        private fun NotificationEntry.setColorizedFgs(colorized: Boolean) {
+            sbn.notification.setColorizedFgs(colorized)
+        }
+
+        private fun Notification.setColorizedFgs(colorized: Boolean) {
+            extras.putBoolean(Notification.EXTRA_COLORIZED, colorized)
+            flags =
+                if (colorized) {
+                    flags or Notification.FLAG_FOREGROUND_SERVICE
+                } else {
+                    flags and Notification.FLAG_FOREGROUND_SERVICE.inv()
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 8b4265f..14134cc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
index 5e87f46..61873ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.statusbar.notification.row.ui.viewmodel
 
+import android.app.Notification
 import android.app.PendingIntent
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -90,7 +91,8 @@
         name: String = "example",
         timeRemaining: Duration = Duration.ofMinutes(3),
         resumeIntent: PendingIntent? = null,
-        resetIntent: PendingIntent? = null
+        addMinuteAction: Notification.Action? = null,
+        resetAction: Notification.Action? = null
     ) =
         TimerContentModel(
             icon = icon,
@@ -99,7 +101,8 @@
                 Paused(
                     timeRemaining = timeRemaining,
                     resumeIntent = resumeIntent,
-                    resetIntent = resetIntent,
+                    addMinuteAction = addMinuteAction,
+                    resetAction = resetAction,
                 )
         )
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index f8e6337..f96cf10 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -43,7 +43,6 @@
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
 import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.statusbar.policy.fakeConfigurationController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 71cd95f..6f09931 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -1096,7 +1096,8 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            // Resets to 1f after communal scene is hidden
+            assertThat(alpha).isEqualTo(1f)
         }
 
     @Test
@@ -1151,7 +1152,7 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            assertThat(alpha).isEqualTo(1f)
         }
 
     @Test
@@ -1208,7 +1209,8 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            // Resets to 1f after communal scene is hidden
+            assertThat(alpha).isEqualTo(1f)
         }
 
     @Test
@@ -1263,7 +1265,7 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            assertThat(alpha).isEqualTo(1f)
         }
 
     private suspend fun TestScope.showLockscreen() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index b643968..c3c5a48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -50,6 +50,7 @@
                 statusBarStateController = statusBarStateController,
                 mainExecutor = mainExecutor,
                 legacyActivityStarter = { legacyActivityStarterInternal },
+                activityStarterInternal = { activityStarterInternal },
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index 10a2f64..d82b9db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -231,7 +231,6 @@
         // extra activity options to set on pending intent
         val activityOptions = mock(ActivityOptions::class.java)
         activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR
-        activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false
         val bundleCaptor = argumentCaptor<Bundle>()
 
         startPendingIntentMaybeDismissingKeyguard(
@@ -255,7 +254,8 @@
                 bundleCaptor.capture()
             )
         val options = ActivityOptions.fromBundle(bundleCaptor.firstValue)
-        assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse()
+        assertThat(options.getPendingIntentBackgroundActivityStartMode())
+            .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS)
         assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR)
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index 495ab61..8f9da3b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -180,6 +180,23 @@
     }
 
     @Test
+    fun testDelete_untracked_runnableRuns() {
+        val headsUpEntry = createHeadsUpEntry(id = 0)
+
+        // None showing
+        mAvalancheController.headsUpEntryShowing = null
+
+        // Nothing is next
+        mAvalancheController.clearNext()
+
+        // Delete
+        mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel")
+
+        // Runnable was run
+        Mockito.verify(runnableMock, Mockito.times(1)).run()
+    }
+
+    @Test
     fun testDelete_isNext_removedFromNext_runnableNotRun() {
         // Entry is next
         val headsUpEntry = createHeadsUpEntry(id = 0)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index d0ddbff..5dadc4c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
@@ -42,6 +43,7 @@
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.mockExecutorHandler
 import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
 import junit.framework.Assert
@@ -237,6 +239,34 @@
     }
 
     @Test
+    fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
+        val hmp = createHeadsUpManagerPhone()
+
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
+        hmp.showNotification(notifEntry)
+        Assert.assertTrue(notifEntry.isSeenInShade)
+    }
+
+    @Test
+    fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
+        val hmp = createHeadsUpManagerPhone()
+
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
+        hmp.showNotification(notifEntry)
+        Assert.assertFalse(notifEntry.isSeenInShade)
+    }
+
+    @Test
     fun shouldHeadsUpBecomePinned_shadeNotExpanded_true() =
         testScope.runTest {
             // GIVEN
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
new file mode 100644
index 0000000..fdfc7f1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ModesDialogViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    val repository = kosmos.fakeZenModeRepository
+    val interactor = kosmos.zenModeInteractor
+
+    val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher)
+
+    @Test
+    fun tiles_filtersOutDisabledModes() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder().setName("Disabled").setEnabled(false).build(),
+                    TestModeBuilder.MANUAL_DND,
+                    TestModeBuilder()
+                        .setName("Enabled")
+                        .setEnabled(true)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Disabled with manual")
+                        .setEnabled(false)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                ))
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(2)
+            with(tiles?.elementAt(0)!!) {
+                assertThat(this.text).isEqualTo("Manual DND")
+                assertThat(this.subtext).isEqualTo("On")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(1)!!) {
+                assertThat(this.text).isEqualTo("Enabled")
+                assertThat(this.subtext).isEqualTo("Off")
+                assertThat(this.enabled).isEqualTo(false)
+            }
+        }
+
+    @Test
+    fun tiles_filtersOutInactiveModesWithoutManualInvocation() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setName("Active without manual")
+                        .setActive(true)
+                        .setManualInvocationAllowed(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Active with manual")
+                        .setTriggerDescription("trigger description")
+                        .setActive(true)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Inactive with manual")
+                        .setActive(false)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Inactive without manual")
+                        .setActive(false)
+                        .setManualInvocationAllowed(false)
+                        .build(),
+                ))
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(3)
+            with(tiles?.elementAt(0)!!) {
+                assertThat(this.text).isEqualTo("Active without manual")
+                assertThat(this.subtext).isEqualTo("On")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(1)!!) {
+                assertThat(this.text).isEqualTo("Active with manual")
+                assertThat(this.subtext).isEqualTo("trigger description")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(2)!!) {
+                assertThat(this.text).isEqualTo("Inactive with manual")
+                assertThat(this.subtext).isEqualTo("Off")
+                assertThat(this.enabled).isEqualTo(false)
+            }
+        }
+
+    @Test
+    fun onClick_togglesTileState() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            val modeId = "id"
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setId(modeId)
+                        .setName("Test")
+                        .setManualInvocationAllowed(true)
+                        .build()
+                )
+            )
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(1)
+            assertThat(tiles?.elementAt(0)?.enabled).isFalse()
+
+            // Trigger onClick
+            tiles?.first()?.onClick?.let { it() }
+            runCurrent()
+
+            assertThat(tiles?.first()?.enabled).isTrue()
+
+            // Trigger onClick
+            tiles?.first()?.onClick?.let { it() }
+            runCurrent()
+
+            assertThat(tiles?.first()?.enabled).isFalse()
+        }
+}
diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
index f2bfbe5c9..3a679e3 100644
--- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
+++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
@@ -33,7 +33,6 @@
         android:id="@+id/icon"
         android:layout_width="24dp"
         android:layout_height="24dp"
-        android:src="@drawable/ic_close"
         app:tint="@android:color/white"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/label"
@@ -88,11 +87,10 @@
         />
 
     <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
         android:id="@+id/mainButton"
         android:layout_width="124dp"
         android:layout_height="wrap_content"
-        tools:text="Reset"
-        tools:drawableStart="@android:drawable/ic_menu_add"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/altButton"
         app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
@@ -101,15 +99,23 @@
         />
 
     <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
         android:id="@+id/altButton"
-        tools:text="Reset"
-        tools:drawableStart="@android:drawable/ic_menu_add"
-        android:drawablePadding="2dp"
-        android:drawableTint="@android:color/white"
         android:layout_width="124dp"
         android:layout_height="wrap_content"
         app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
         app:layout_constraintStart_toEndOf="@id/mainButton"
+        app:layout_constraintEnd_toEndOf="@id/resetButton"
+        android:paddingEnd="4dp"
+        />
+
+    <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
+        android:id="@+id/resetButton"
+        android:layout_width="124dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+        app:layout_constraintStart_toEndOf="@id/altButton"
         app:layout_constraintEnd_toEndOf="parent"
         android:paddingEnd="4dp"
         />
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 763930d..07a40c8 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -33,9 +33,4 @@
     <!--  Whether the user switcher chip shows in the status bar. When true, the multi user
       avatar will no longer show on the lockscreen -->
     <bool name="flag_user_switcher_chip">false</bool>
-
-    <!-- Whether the battery icon is allowed to display a shield when battery life is being
-         protected. -->
-    <bool name="flag_battery_shield_icon">false</bool>
-
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7caa2c6..68c83c7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1093,6 +1093,12 @@
     <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] -->
     <string name="zen_modes_dialog_settings">Settings</string>
 
+    <!-- Priority modes: label for an active mode [CHAR LIMIT=35] -->
+    <string name="zen_mode_on">On</string>
+
+    <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] -->
+    <string name="zen_mode_off">Off</string>
+
     <!-- Zen mode: Priority only introduction message on first use -->
     <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string>
 
@@ -1258,6 +1264,8 @@
     <string name="communal_widget_picker_title">Lock screen widgets</string>
     <!-- Text displayed below the title in the communal widget picker providing additional details about the communal surface. [CHAR LIMIT=80] -->
     <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string>
+    <!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_unselect_widget">unselect widget</string>
     <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
     <string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
     <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
@@ -3669,4 +3677,31 @@
     <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
     <!-- Label for volume undo action [CHAR LIMIT=NONE] -->
     <string name="volume_undo_action">Undo</string>
+
+    <!-- Keyboard touchpad contextual education strings-->
+    <!-- Education toast text for Back [CHAR_LIMIT=100] -->
+    <string name="back_edu_toast_content">To go back, swipe left or right with three fingers on the touchpad</string>
+    <!-- Education toast text for Home [CHAR_LIMIT=100] -->
+    <string name="home_edu_toast_content">To go home, swipe up with three fingers on the touchpad</string>
+    <!-- Education toast text for Overview [CHAR_LIMIT=100] -->
+    <string name="overview_edu_toast_content">To view recent apps, swipe up and hold with three fingers on the touchpad</string>
+    <!-- Education toast text for All Apps [CHAR_LIMIT=100] -->
+    <string name="all_apps_edu_toast_content">To view all your apps, press the action key on your keyboard</string>
+
+    <!-- Education notification title for Back [CHAR_LIMIT=100] -->
+    <string name="back_edu_notification_title">Use your touchpad to go back</string>
+    <!-- Education notification text for Back [CHAR_LIMIT=100] -->
+    <string name="back_edu_notification_content">Swipe left or right using three fingers. Tap to learn more gestures.</string>
+    <!-- Education notification title for Home [CHAR_LIMIT=100] -->
+    <string name="home_edu_notification_title">Use your touchpad to go home</string>
+    <!-- Education notification text for Home [CHAR_LIMIT=100] -->
+    <string name="home_edu_notification_content">Swipe up using three fingers. Tap to learn more gestures.</string>
+    <!-- Education notification title for Overview [CHAR_LIMIT=100] -->
+    <string name="overview_edu_notification_title">Use your touchpad to view recent apps</string>
+    <!-- Education notification text for Overview [CHAR_LIMIT=100] -->
+    <string name="overview_edu_notification_content">Swipe up and hold using three fingers. Tap to learn more gestures.</string>
+    <!-- Education notification title for All Apps [CHAR_LIMIT=100] -->
+    <string name="all_apps_edu_notification_title">Use your keyboard to view all apps</string>
+    <!-- Education notification text for All Apps [CHAR_LIMIT=100] -->
+    <string name="all_apps_edu_notification_content">Press the action key at any time. Tap to learn more gestures.</string>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 845ca5e..3019fe7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -344,7 +344,7 @@
      * Shows a voice session identified by {@code token}
      * @return true if the session was shown, false otherwise
      */
-    public boolean showVoiceSession(@NonNull IBinder token, @NonNull Bundle args, int flags,
+    public boolean showVoiceSession(IBinder token, @NonNull Bundle args, int flags,
             @Nullable String attributionTag) {
         IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface(
                 ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 5c53234..e634726 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -88,7 +88,6 @@
     private boolean mPowerSaveEnabled;
     private boolean mIsBatteryDefender;
     private boolean mIsIncompatibleCharging;
-    private boolean mDisplayShieldEnabled;
     // Error state where we know nothing about the current battery state
     private boolean mBatteryStateUnknown;
     // Lazily-loaded since this is expected to be a rare-if-ever state
@@ -270,7 +269,7 @@
         int resId = 0;
         if (mPowerSaveEnabled) {
             resId = R.drawable.battery_unified_attr_powersave;
-        } else if (mIsBatteryDefender && mDisplayShieldEnabled) {
+        } else if (mIsBatteryDefender) {
             resId = R.drawable.battery_unified_attr_defend;
         } else if (isCharging) {
             resId = R.drawable.battery_unified_attr_charging;
@@ -288,7 +287,7 @@
     private ColorProfile getCurrentColorProfile() {
         return getColorProfile(
                 mPowerSaveEnabled,
-                mIsBatteryDefender && mDisplayShieldEnabled,
+                mIsBatteryDefender,
                 mPluggedIn,
                 mLevel <= 20);
     }
@@ -410,10 +409,6 @@
         mBatteryEstimateFetcher = fetcher;
     }
 
-    void setDisplayShieldEnabled(boolean displayShieldEnabled) {
-        mDisplayShieldEnabled = displayShieldEnabled;
-    }
-
     void updatePercentText() {
         if (!newStatusBarIcons()) {
             updatePercentTextLegacy();
@@ -659,7 +654,7 @@
         float mainBatteryWidth =
                 res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
 
-        boolean displayShield = mDisplayShieldEnabled && mIsBatteryDefender;
+        boolean displayShield = mIsBatteryDefender;
         float fullBatteryIconHeight =
                 BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
         float fullBatteryIconWidth =
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index 4f13e6f..9a30c21 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -34,7 +34,6 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
@@ -153,8 +152,6 @@
         mBatteryController = batteryController;
 
         mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
-        mView.setDisplayShieldEnabled(
-                getContext().getResources().getBoolean(R.bool.flag_battery_shield_icon));
 
         mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
         mSettingObserver = new SettingObserver(mMainHandler);
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
index 85e2bdb..b6ace81 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -19,10 +19,14 @@
 
 import android.annotation.SuppressLint
 import android.content.Context
+import android.os.Bundle
 import android.util.AttributeSet
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewConfiguration
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import com.android.systemui.shade.TouchLogger
 import kotlin.math.pow
 import kotlin.math.sqrt
@@ -44,6 +48,10 @@
         attrs,
     ) {
 
+    init {
+        setupAccessibilityDelegate()
+    }
+
     constructor(
         context: Context,
         attrs: AttributeSet?,
@@ -55,6 +63,7 @@
             view: View,
             x: Int,
             y: Int,
+            isA11yAction: Boolean = false,
         )
 
         /** Notifies that the gesture was too short for a long press, it is actually a click. */
@@ -63,6 +72,8 @@
 
     var listener: Listener? = null
 
+    var accessibilityHintLongPressAction: AccessibilityAction? = null
+
     private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
         LongPressHandlingViewInteractionHandler(
             postDelayed = { block, timeoutMs ->
@@ -107,6 +118,51 @@
     override fun onTouchEvent(event: MotionEvent?): Boolean {
         return interactionHandler.onTouchEvent(event?.toModel())
     }
+
+    private fun setupAccessibilityDelegate() {
+        accessibilityDelegate =
+            object : AccessibilityDelegate() {
+                override fun onInitializeAccessibilityNodeInfo(
+                    v: View,
+                    info: AccessibilityNodeInfo
+                ) {
+                    super.onInitializeAccessibilityNodeInfo(v, info)
+                    if (
+                        interactionHandler.isLongPressHandlingEnabled &&
+                            accessibilityHintLongPressAction != null
+                    ) {
+                        info.addAction(accessibilityHintLongPressAction)
+                    }
+                }
+
+                override fun performAccessibilityAction(
+                    host: View,
+                    action: Int,
+                    args: Bundle?
+                ): Boolean {
+                    return if (
+                        interactionHandler.isLongPressHandlingEnabled &&
+                            action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK
+                    ) {
+                        val longPressHandlingView = host as? LongPressHandlingView
+                        if (longPressHandlingView != null) {
+                            // the coordinates are not available as it is an a11y long press
+                            listener?.onLongPressDetected(
+                                view = longPressHandlingView,
+                                x = 0,
+                                y = 0,
+                                isA11yAction = true,
+                            )
+                            true
+                        } else {
+                            false
+                        }
+                    } else {
+                        super.performAccessibilityAction(host, action, args)
+                    }
+                }
+            }
+    }
 }
 
 private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
index d8067b8..4de39c4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
@@ -49,14 +49,8 @@
     /** Whether the CTA tile has been dismissed. */
     fun isCtaDismissed(user: UserInfo): Flow<Boolean>
 
-    /** Whether the lock screen widget disclaimer has been dismissed by the user. */
-    fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean>
-
     /** Save the CTA tile dismissed state for the current user. */
     suspend fun setCtaDismissed(user: UserInfo)
-
-    /** Save the lock screen widget disclaimer dismissed state for the current user. */
-    suspend fun setDisclaimerDismissed(user: UserInfo)
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -74,9 +68,6 @@
     override fun isCtaDismissed(user: UserInfo): Flow<Boolean> =
         readKeyForUser(user, CTA_DISMISSED_STATE)
 
-    override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> =
-        readKeyForUser(user, DISCLAIMER_DISMISSED_STATE)
-
     /**
      * Emits an event each time a Backup & Restore restoration job is completed, and once at the
      * start of collection.
@@ -97,12 +88,6 @@
             logger.i("Dismissed CTA tile")
         }
 
-    override suspend fun setDisclaimerDismissed(user: UserInfo) =
-        withContext(bgDispatcher) {
-            getSharedPrefsForUser(user).edit().putBoolean(DISCLAIMER_DISMISSED_STATE, true).apply()
-            logger.i("Dismissed widget disclaimer")
-        }
-
     private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences {
         return userFileManager.getSharedPreferences(
             FILE_NAME,
@@ -124,6 +109,5 @@
         const val TAG = "CommunalPrefsRepository"
         const val FILE_NAME = "communal_hub_prefs"
         const val CTA_DISMISSED_STATE = "cta_dismissed"
-        const val DISCLAIMER_DISMISSED_STATE = "disclaimer_dismissed"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 3fffd76..e13161f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -23,6 +23,7 @@
 import android.os.UserHandle
 import android.os.UserManager
 import android.provider.Settings
+import com.android.app.tracing.coroutines.launch
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
@@ -64,10 +65,12 @@
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.emitOnStart
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.minutes
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -94,6 +97,7 @@
 @Inject
 constructor(
     @Application val applicationScope: CoroutineScope,
+    @Background private val bgScope: CoroutineScope,
     @Background val bgDispatcher: CoroutineDispatcher,
     broadcastDispatcher: BroadcastDispatcher,
     private val widgetRepository: CommunalWidgetRepository,
@@ -148,6 +152,17 @@
                 replay = 1,
             )
 
+    private val _isDisclaimerDismissed = MutableStateFlow(false)
+    val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow()
+
+    fun setDisclaimerDismissed() {
+        bgScope.launch("$TAG#setDisclaimerDismissed") {
+            _isDisclaimerDismissed.value = true
+            delay(DISCLAIMER_RESET_MILLIS)
+            _isDisclaimerDismissed.value = false
+        }
+    }
+
     /** Whether to show communal when exiting the occluded state. */
     val showCommunalFromOccluded: Flow<Boolean> =
         keyguardTransitionInteractor.startedKeyguardTransitionStep
@@ -510,6 +525,14 @@
     }
 
     companion object {
+        const val TAG = "CommunalInteractor"
+
+        /**
+         * The amount of time between showing the widget disclaimer to the user as measured from the
+         * moment the disclaimer is dimsissed.
+         */
+        val DISCLAIMER_RESET_MILLIS = 30.minutes
+
         /**
          * The user activity timeout which should be used when the communal hub is opened. A value
          * of -1 means that the user's chosen screen timeout will be used instead.
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt
index 3517650..0b5f40d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.communal.domain.interactor
 
 import android.content.pm.UserInfo
-import com.android.app.tracing.coroutines.launch
 import com.android.systemui.communal.data.repository.CommunalPrefsRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -43,7 +42,7 @@
     private val repository: CommunalPrefsRepository,
     userInteractor: SelectedUserInteractor,
     private val userTracker: UserTracker,
-    @CommunalTableLog tableLogBuffer: TableLogBuffer
+    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) {
 
     val isCtaDismissed: Flow<Boolean> =
@@ -64,25 +63,6 @@
     suspend fun setCtaDismissed(user: UserInfo = userTracker.userInfo) =
         repository.setCtaDismissed(user)
 
-    val isDisclaimerDismissed: Flow<Boolean> =
-        userInteractor.selectedUserInfo
-            .flatMapLatest { user -> repository.isDisclaimerDismissed(user) }
-            .logDiffsForTable(
-                tableLogBuffer = tableLogBuffer,
-                columnPrefix = "",
-                columnName = "isDisclaimerDismissed",
-                initialValue = false,
-            )
-            .stateIn(
-                scope = bgScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
-            )
-
-    fun setDisclaimerDismissed(user: UserInfo = userTracker.userInfo) {
-        bgScope.launch("$TAG#setDisclaimerDismissed") { repository.setDisclaimerDismissed(user) }
-    }
-
     private companion object {
         const val TAG = "CommunalPrefsInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 7b0aadf..0353d2c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -82,10 +82,10 @@
         communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING }
 
     val showDisclaimer: Flow<Boolean> =
-        allOf(isCommunalContentVisible, not(communalPrefsInteractor.isDisclaimerDismissed))
+        allOf(isCommunalContentVisible, not(communalInteractor.isDisclaimerDismissed))
 
     fun onDisclaimerDismissed() {
-        communalPrefsInteractor.setDisclaimerDismissed()
+        communalInteractor.setDisclaimerDismissed()
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
index 7f8103e..6864f4e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
@@ -18,7 +18,7 @@
 
 import android.app.Activity
 import android.app.ActivityOptions
-import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
 import android.app.Dialog
 import android.app.PendingIntent
 import android.content.ComponentName
@@ -93,8 +93,8 @@
                 0 /* enterResId */,
                 0 /* exitResId */
             ).apply {
-                pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+                pendingIntentBackgroundActivityStartMode =
+                    MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
                 taskAlwaysOnTop = true
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
index 53b9261..0e2e2e6 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -16,12 +16,21 @@
 
 package com.android.systemui.education.dagger
 
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.education.data.repository.ContextualEducationRepository
 import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl
+import com.android.systemui.education.domain.interactor.ContextualEducationInteractor
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
+import com.android.systemui.shared.education.GestureType
 import dagger.Binds
+import dagger.Lazy
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 import java.time.Clock
 import javax.inject.Qualifier
 import kotlinx.coroutines.CoroutineDispatcher
@@ -53,5 +62,41 @@
         fun provideEduClock(): Clock {
             return Clock.systemUTC()
         }
+
+        @Provides
+        @IntoMap
+        @ClassKey(ContextualEducationInteractor::class)
+        fun provideContextualEducationInteractor(
+            implLazy: Lazy<ContextualEducationInteractor>
+        ): CoreStartable {
+            return if (Flags.keyboardTouchpadContextualEducation()) {
+                implLazy.get()
+            } else {
+                // No-op implementation when the flag is disabled.
+                return NoOpCoreStartable
+            }
+        }
+
+        @Provides
+        fun provideKeyboardTouchpadEduStatsInteractor(
+            implLazy: Lazy<KeyboardTouchpadEduStatsInteractorImpl>
+        ): KeyboardTouchpadEduStatsInteractor {
+            return if (Flags.keyboardTouchpadContextualEducation()) {
+                implLazy.get()
+            } else {
+                // No-op implementation when the flag is disabled.
+                return NoOpKeyboardTouchpadEduStatsInteractor
+            }
+        }
+    }
+
+    private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
+        override fun incrementSignalCount(gestureType: GestureType) {}
+
+        override fun updateShortcutTriggerTime(gestureType: GestureType) {}
+    }
+
+    private object NoOpCoreStartable : CoreStartable {
+        override fun start() {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
new file mode 100644
index 0000000..e2aa911
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.systemui.education.domain.interactor
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.education.data.repository.ContextualEducationRepository
+import com.android.systemui.shared.education.GestureType
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/**
+ * Allows updating education data (e.g. signal count, shortcut time) for different gesture types.
+ * Change user education repository when user is changed.
+ */
+@SysUISingleton
+class ContextualEducationInteractor
+@Inject
+constructor(
+    @Background private val backgroundScope: CoroutineScope,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val repository: ContextualEducationRepository,
+) : CoreStartable {
+
+    override fun start() {
+        backgroundScope.launch {
+            selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) }
+        }
+    }
+
+    suspend fun incrementSignalCount(gestureType: GestureType) =
+        repository.incrementSignalCount(gestureType)
+
+    suspend fun updateShortcutTriggerTime(gestureType: GestureType) =
+        repository.updateShortcutTriggerTime(gestureType)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
new file mode 100644
index 0000000..643e571
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.systemui.education.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.education.GestureType
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Encapsulates the update functions of KeyboardTouchpadEduStatsInteractor. This encapsulation is
+ * for having a different implementation of interactor when the feature flag is off.
+ */
+interface KeyboardTouchpadEduStatsInteractor {
+    fun incrementSignalCount(gestureType: GestureType)
+
+    fun updateShortcutTriggerTime(gestureType: GestureType)
+}
+
+/** Allow update to education data related to keyboard/touchpad. */
+@SysUISingleton
+class KeyboardTouchpadEduStatsInteractorImpl
+@Inject
+constructor(
+    @Background private val backgroundScope: CoroutineScope,
+    private val contextualEducationInteractor: ContextualEducationInteractor
+) : KeyboardTouchpadEduStatsInteractor {
+
+    override fun incrementSignalCount(gestureType: GestureType) {
+        // Todo: check if keyboard/touchpad is connected before update
+        backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) }
+    }
+
+    override fun updateShortcutTriggerTime(gestureType: GestureType) {
+        backgroundScope.launch {
+            contextualEducationInteractor.updateShortcutTriggerTime(gestureType)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index af7ecf6..0e06117 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.ComposeLockscreen
+import com.android.systemui.qs.flags.NewQsUI
+import com.android.systemui.qs.flags.QSComposeFragment
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
@@ -35,6 +37,8 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
@@ -53,6 +57,7 @@
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
         PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
+        NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
 
         // SceneContainer dependencies
         SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
@@ -66,14 +71,20 @@
 
         // DualShade dependencies
         DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag()
+
+        // QS Fragment using Compose dependencies
+        QSComposeFragment.token dependsOn NewQsUI.token
     }
 
     private inline val politeNotifications
         get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications())
+
     private inline val crossAppPoliteNotifications
         get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
+
     private inline val vibrateWhileUnlockedToken: FlagToken
         get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
+
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index ec03a6d..046e79c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -188,7 +188,7 @@
      * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
      * but not vice-versa.
      */
-    val isDreaming: Flow<Boolean> = repository.isDreaming
+    val isDreaming: StateFlow<Boolean> = repository.isDreaming
 
     /** Whether the system is dreaming with an overlay active */
     val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
@@ -205,7 +205,8 @@
                         trySendWithFailureLogging(
                             cameraLaunchSourceIntToModel(source),
                             TAG,
-                            "updated onCameraLaunchGestureDetected")
+                            "updated onCameraLaunchGestureDetected"
+                        )
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index 7a06d2f..beb54c7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -129,13 +129,16 @@
         }
     }
 
-    /** Notifies that the user has long-pressed on the lock screen. */
-    fun onLongPress() {
+    /** Notifies that the user has long-pressed on the lock screen.
+     *
+     * @param isA11yAction: Whether the action was performed as an a11y action
+     */
+    fun onLongPress(isA11yAction: Boolean = false) {
         if (!isLongPressHandlingEnabled.value) {
             return
         }
 
-        if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) {
+        if (isA11yAction || featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) {
             showSettings()
         } else {
             showMenu()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 76f7749..1b9788f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -74,8 +74,8 @@
         val bgView = view.bgView
         longPressHandlingView.listener =
             object : LongPressHandlingView.Listener {
-                override fun onLongPressDetected(view: View, x: Int, y: Int) {
-                    if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+                override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
+                    if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
                         return
                     }
                     vibratorHelper.performHapticFeedback(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
index 057b4f9..b387855 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
@@ -18,12 +18,15 @@
 package com.android.systemui.keyguard.ui.binder
 
 import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launch
 import com.android.systemui.common.ui.view.LongPressHandlingView
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
 import com.android.systemui.plugins.FalsingManager
 
 object KeyguardLongPressViewBinder {
@@ -43,14 +46,19 @@
         onSingleTap: () -> Unit,
         falsingManager: FalsingManager,
     ) {
+        view.accessibilityHintLongPressAction =
+            AccessibilityNodeInfo.AccessibilityAction(
+                AccessibilityNodeInfoCompat.ACTION_LONG_CLICK,
+                view.resources.getString(R.string.lock_screen_settings)
+            )
         view.listener =
             object : LongPressHandlingView.Listener {
-                override fun onLongPressDetected(view: View, x: Int, y: Int) {
-                    if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+                override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
+                    if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
                         return
                     }
 
-                    viewModel.onLongPress()
+                    viewModel.onLongPress(isA11yAction)
                 }
 
                 override fun onSingleTapDetected(view: View) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index bb4fb79..e9db1d2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -83,6 +83,7 @@
                     MathUtils.lerp(startAlpha, 0f, it)
                 }
             },
+            onFinish = { 1f },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index f1cbf25..1d2edc6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -33,9 +33,12 @@
     /** Whether the long-press handling feature should be enabled. */
     val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
 
-    /** Notifies that the user has long-pressed on the lock screen. */
-    fun onLongPress() {
-        interactor.onLongPress()
+    /** Notifies that the user has long-pressed on the lock screen.
+     *
+     * @param isA11yAction: Whether the action was performed as an a11y action
+     */
+    fun onLongPress(isA11yAction: Boolean) {
+        interactor.onLongPress(isA11yAction)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 46ba5d1..8811908 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -80,6 +80,7 @@
                     1f - it
                 }
             },
+            onFinish = { 1f },
         )
 
     /** Bouncer container alpha */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
index 8af5665..ee709c4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the notification avalanche suppression flag state. */
+/** Helper for reading or using the new QS UI flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NewQsUI {
     /** The aconfig flag name */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt
new file mode 100644
index 0000000..664d496
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.flags
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the new QS UI in NPVC flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object QSComposeFragment {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.qsUiRefactorComposeFragment() && NewQsUI.isEnabled
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 720120b..5ea8c21 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -14,6 +14,8 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ArgbEvaluator;
@@ -204,6 +206,9 @@
             values.setEvaluator(ArgbEvaluator.getInstance());
             mColorAnimator.setValues(values);
             mColorAnimator.removeAllListeners();
+            if (removeUpdateListenerInQsIconViewImpl()) {
+                mColorAnimator.removeAllUpdateListeners();
+            }
             mColorAnimator.addUpdateListener(animation -> {
                 setTint(iv, (int) animation.getAnimatedValue());
             });
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 51447cc..72f37fc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -187,7 +187,6 @@
         applicationScope.launch {
             // TODO(b/296114544): Combine with some global hun state to make it visible!
             deviceProvisioningInteractor.isDeviceProvisioned
-                .distinctUntilChanged()
                 .flatMapLatest { isAllowedToBeVisible ->
                     if (isAllowedToBeVisible) {
                         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 4f6a64f..bd08685 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -1265,20 +1265,20 @@
             mTranslationForFullShadeTransition = qsTranslation;
             updateQsFrameTranslation();
             float currentTranslation = mQsFrame.getTranslationY();
-            int clipTop = mEnableClipping
-                    ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0;
-            int clipBottom = mEnableClipping
-                    ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0;
+            int clipTop = (int) (top - currentTranslation - mQsFrame.getTop());
+            int clipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop());
             mVisible = qsVisible;
             mQs.setQsVisible(qsVisible);
-            mQs.setFancyClipping(
-                    mDisplayLeftInset,
-                    clipTop,
-                    mDisplayRightInset,
-                    clipBottom,
-                    radius,
-                    qsVisible && !mSplitShadeEnabled,
-                    mIsFullWidth);
+            if (mEnableClipping) {
+                mQs.setFancyClipping(
+                        mDisplayLeftInset,
+                        clipTop,
+                        mDisplayRightInset,
+                        clipBottom,
+                        radius,
+                        qsVisible && !mSplitShadeEnabled,
+                        mIsFullWidth);
+            }
 
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
index b2140f7..130b117 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.res.ColorStateList
+import android.view.ContextThemeWrapper
 import androidx.annotation.ColorInt
 import com.android.settingslib.Utils
 import com.android.systemui.res.R
@@ -41,8 +42,11 @@
 
     /** The chip should have a red background with white text. */
     data object Red : ColorsModel {
-        override fun background(context: Context): ColorStateList =
-            ColorStateList.valueOf(context.getColor(R.color.GM2_red_600))
+        override fun background(context: Context): ColorStateList {
+            val themedContext =
+                ContextThemeWrapper(context, com.android.internal.R.style.Theme_DeviceDefault_Light)
+            return Utils.getColorAttr(themedContext, com.android.internal.R.attr.materialColorError)
+        }
 
         override fun text(context: Context) = context.getColor(android.R.color.white)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index 3dcaff3..b342722 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -52,8 +52,11 @@
     }
 
     fun getNotificationBuckets(): IntArray {
-        if (PriorityPeopleSection.isEnabled || NotificationMinimalismPrototype.V2.isEnabled
-            || NotificationClassificationFlag.isEnabled) {
+        if (
+            PriorityPeopleSection.isEnabled ||
+                NotificationMinimalismPrototype.isEnabled ||
+                NotificationClassificationFlag.isEnabled
+        ) {
             // We don't need this list to be adaptive, it can be the superset of all features.
             return PriorityBucket.getAllInOrder()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index e48c28d..cb133ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -1005,6 +1005,16 @@
         mIsMarkedForUserTriggeredMovement = marked;
     }
 
+    private boolean mSeenInShade = false;
+
+    public void setSeenInShade(boolean seen) {
+        mSeenInShade = seen;
+    }
+
+    public boolean isSeenInShade() {
+        return mSeenInShade;
+    }
+
     public void setIsHeadsUpEntry(boolean isHeadsUpEntry) {
         mIsHeadsUpEntry = isHeadsUpEntry;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
new file mode 100644
index 0000000..a6605f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -0,0 +1,295 @@
+/*
+ * 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.statusbar.notification.collection.coordinator
+
+import android.annotation.SuppressLint
+import android.app.NotificationManager
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+/**
+ * If the setting is enabled, this will track seen notifications and ensure that they only show in
+ * the shelf on the lockscreen.
+ *
+ * This class is a replacement of the [OriginalUnseenKeyguardCoordinator].
+ */
+@CoordinatorScope
+@SuppressLint("SharedFlowCreation")
+class LockScreenMinimalismCoordinator
+@Inject
+constructor(
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val dumpManager: DumpManager,
+    private val headsUpInteractor: HeadsUpNotificationInteractor,
+    private val logger: LockScreenMinimalismCoordinatorLogger,
+    @Application private val scope: CoroutineScope,
+    private val secureSettings: SecureSettings,
+    private val seenNotificationsInteractor: SeenNotificationsInteractor,
+    private val statusBarStateController: StatusBarStateController,
+    private val shadeInteractor: ShadeInteractor,
+) : Coordinator, Dumpable {
+
+    private val unseenNotifications = mutableSetOf<NotificationEntry>()
+    private var isShadeVisible = false
+    private var unseenFilterEnabled = false
+
+    override fun attach(pipeline: NotifPipeline) {
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+            return
+        }
+        pipeline.addPromoter(unseenNotifPromoter)
+        pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
+        pipeline.addCollectionListener(collectionListener)
+        scope.launch { trackUnseenFilterSettingChanges() }
+        dumpManager.registerDumpable(this)
+    }
+
+    private suspend fun trackSeenNotifications() {
+        coroutineScope {
+            launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+            launch { markHeadsUpNotificationsAsSeen() }
+        }
+    }
+
+    private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+        shadeInteractor.isShadeFullyExpanded.collectLatest { isExpanded ->
+            // Give keyguard events time to propagate, in case this expansion is part of the
+            // keyguard transition and not the user expanding the shade
+            delay(SHADE_VISIBLE_SEEN_TIMEOUT)
+            isShadeVisible = isExpanded
+            if (isExpanded) {
+                logger.logShadeVisible(unseenNotifications.size)
+                unseenNotifications.clear()
+                // no need to invalidateList; filtering is inactive while shade is open
+            } else {
+                logger.logShadeHidden()
+            }
+        }
+    }
+
+    private suspend fun markHeadsUpNotificationsAsSeen() {
+        headsUpInteractor.topHeadsUpRowIfPinned
+            .map { it?.let { headsUpInteractor.notificationKey(it) } }
+            .collectLatest { key ->
+                if (key == null) {
+                    logger.logTopHeadsUpRow(key = null, wasUnseenWhenPinned = false)
+                } else {
+                    val wasUnseenWhenPinned = unseenNotifications.any { it.key == key }
+                    logger.logTopHeadsUpRow(key, wasUnseenWhenPinned)
+                    if (wasUnseenWhenPinned) {
+                        delay(HEADS_UP_SEEN_TIMEOUT)
+                        val wasUnseenAfterDelay = unseenNotifications.removeIf { it.key == key }
+                        logger.logHunHasBeenSeen(key, wasUnseenAfterDelay)
+                        // no need to invalidateList; nothing should change until after heads up
+                    }
+                }
+            }
+    }
+
+    private fun unseenFeatureEnabled(): Flow<Boolean> {
+        // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
+        //  Or should we actually just repurpose using the existing setting?
+        if (NotificationMinimalismPrototype.isEnabled) {
+            return flowOf(true)
+        }
+        return secureSettings
+            // emit whenever the setting has changed
+            .observerFlow(
+                UserHandle.USER_ALL,
+                Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+            )
+            // perform a query immediately
+            .onStart { emit(Unit) }
+            // for each change, lookup the new value
+            .map {
+                secureSettings.getIntForUser(
+                    name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    def = 0,
+                    userHandle = UserHandle.USER_CURRENT,
+                ) == 1
+            }
+            // don't emit anything if nothing has changed
+            .distinctUntilChanged()
+            // perform lookups on the bg thread pool
+            .flowOn(bgDispatcher)
+            // only track the most recent emission, if events are happening faster than they can be
+            // consumed
+            .conflate()
+    }
+
+    private suspend fun trackUnseenFilterSettingChanges() {
+        unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+            // update local field and invalidate if necessary
+            if (isSettingEnabled != unseenFilterEnabled) {
+                unseenFilterEnabled = isSettingEnabled
+                unseenNotifPromoter.invalidateList("unseen setting changed")
+            }
+            // if the setting is enabled, then start tracking and filtering unseen notifications
+            logger.logTrackingUnseen(isSettingEnabled)
+            if (isSettingEnabled) {
+                trackSeenNotifications()
+            }
+        }
+    }
+
+    private val collectionListener =
+        object : NotifCollectionListener {
+            override fun onEntryAdded(entry: NotificationEntry) {
+                if (!isShadeVisible) {
+                    logger.logUnseenAdded(entry.key)
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryUpdated(entry: NotificationEntry) {
+                if (!isShadeVisible) {
+                    logger.logUnseenUpdated(entry.key)
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                if (unseenNotifications.remove(entry)) {
+                    logger.logUnseenRemoved(entry.key)
+                }
+            }
+        }
+
+    private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        // Only ever elevate a top unseen notification on keyguard, not even locked shade
+        if (statusBarStateController.state != StatusBarState.KEYGUARD) {
+            seenNotificationsInteractor.setTopOngoingNotification(null)
+            seenNotificationsInteractor.setTopUnseenNotification(null)
+            return
+        }
+        // On keyguard pick the top-ranked unseen or ongoing notification to elevate
+        val nonSummaryEntries: Sequence<NotificationEntry> =
+            list
+                .asSequence()
+                .flatMap {
+                    when (it) {
+                        is NotificationEntry -> listOfNotNull(it)
+                        is GroupEntry -> it.children
+                        else -> error("unhandled type of $it")
+                    }
+                }
+                .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
+        seenNotificationsInteractor.setTopOngoingNotification(
+            nonSummaryEntries
+                .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
+                .minByOrNull { it.ranking.rank }
+        )
+        seenNotificationsInteractor.setTopUnseenNotification(
+            nonSummaryEntries
+                .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
+                .minByOrNull { it.ranking.rank }
+        )
+    }
+
+    @VisibleForTesting
+    val unseenNotifPromoter =
+        object : NotifPromoter(TAG) {
+            override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
+                when {
+                    NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+                    seenNotificationsInteractor.isTopOngoingNotification(child) -> true
+                    !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+                    else -> seenNotificationsInteractor.isTopUnseenNotification(child)
+                }
+        }
+
+    val topOngoingSectioner =
+        object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
+            override fun isInSection(entry: ListEntry): Boolean {
+                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                return entry.anyEntry { notificationEntry ->
+                    seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
+                }
+            }
+        }
+
+    val topUnseenSectioner =
+        object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
+            override fun isInSection(entry: ListEntry): Boolean {
+                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                return entry.anyEntry { notificationEntry ->
+                    seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
+                }
+            }
+        }
+
+    private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
+        when {
+            predicate(representativeEntry) -> true
+            this !is GroupEntry -> false
+            else -> children.any(predicate)
+        }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) =
+        with(pw.asIndenting()) {
+            seenNotificationsInteractor.dump(this)
+            printCollection("unseen notifications", unseenNotifications) { println(it.key) }
+        }
+
+    companion object {
+        private const val TAG = "LockScreenMinimalismCoordinator"
+        private val SHADE_VISIBLE_SEEN_TIMEOUT = 0.25.seconds
+        private val HEADS_UP_SEEN_TIMEOUT = 0.75.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
new file mode 100644
index 0000000..e44a77c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.statusbar.notification.collection.coordinator
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.UnseenNotificationLog
+import javax.inject.Inject
+
+private const val TAG = "LockScreenMinimalismCoordinator"
+
+class LockScreenMinimalismCoordinatorLogger
+@Inject
+constructor(
+    @UnseenNotificationLog private val buffer: LogBuffer,
+) {
+
+    fun logTrackingUnseen(trackingUnseen: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { bool1 = trackingUnseen },
+            messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+        )
+
+    fun logShadeVisible(numUnseen: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { int1 = numUnseen },
+            messagePrinter = { "Shade expanded. Notifications marked as seen: $int1" }
+        )
+    }
+
+    fun logShadeHidden() {
+        buffer.log(TAG, LogLevel.DEBUG, "Shade no longer expanded.")
+    }
+
+    fun logUnseenAdded(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif added: $str1" },
+        )
+
+    fun logUnseenUpdated(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif updated: $str1" },
+        )
+
+    fun logUnseenRemoved(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif removed: $str1" },
+        )
+
+    fun logHunHasBeenSeen(key: String, wasUnseen: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = {
+                str1 = key
+                bool1 = wasUnseen
+            },
+            messagePrinter = { "Heads up notif has been seen: $str1 wasUnseen=$bool1" },
+        )
+
+    fun logTopHeadsUpRow(key: String?, wasUnseenWhenPinned: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = {
+                str1 = key
+                bool1 = wasUnseenWhenPinned
+            },
+            messagePrinter = { "New notif is top heads up: $str1 wasUnseen=$bool1" },
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 99327d1..73ce48b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -47,6 +47,7 @@
     hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
     keyguardCoordinator: KeyguardCoordinator,
     unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+    lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator,
     rankingCoordinator: RankingCoordinator,
     colorizedFgsCoordinator: ColorizedFgsCoordinator,
     deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -87,7 +88,11 @@
         mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
         mCoordinators.add(hideNotifsForOtherUsersCoordinator)
         mCoordinators.add(keyguardCoordinator)
-        mCoordinators.add(unseenKeyguardCoordinator)
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mCoordinators.add(lockScreenMinimalismCoordinator)
+        } else {
+            mCoordinators.add(unseenKeyguardCoordinator)
+        }
         mCoordinators.add(rankingCoordinator)
         mCoordinators.add(colorizedFgsCoordinator)
         mCoordinators.add(deviceProvisionedCoordinator)
@@ -120,12 +125,12 @@
         }
 
         // Manually add Ordered Sections
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
         }
         mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
         }
         mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
         if (PriorityPeopleSection.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index 5dd1663..5b25b11 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.annotation.SuppressLint
-import android.app.NotificationManager
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
@@ -30,21 +29,14 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.expansionChanges
-import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.headsUpEvents
 import com.android.systemui.util.asIndenting
@@ -73,9 +65,12 @@
 import kotlinx.coroutines.yield
 
 /**
- * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
- * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the
- * lockscreen.
+ * If the setting is enabled, this will track and hide seen notifications on the lockscreen.
+ *
+ * This is the "original" unseen keyguard coordinator because this is the logic originally developed
+ * for large screen devices where showing "seen" notifications on the lock screen was distracting.
+ * Moreover, this file was created during a project that will replace this logic, so the
+ * [LockScreenMinimalismCoordinator] is the expected replacement of this file.
  */
 @CoordinatorScope
 @SuppressLint("SharedFlowCreation")
@@ -100,10 +95,7 @@
     private var unseenFilterEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            pipeline.addPromoter(unseenNotifPromoter)
-            pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
-        }
+        NotificationMinimalismPrototype.assertInLegacyMode()
         pipeline.addFinalizeFilter(unseenNotifFilter)
         pipeline.addCollectionListener(collectionListener)
         scope.launch { trackUnseenFilterSettingChanges() }
@@ -112,6 +104,7 @@
 
     private suspend fun trackSeenNotifications() {
         // Whether or not keyguard is visible (or occluded).
+        @Suppress("DEPRECATION")
         val isKeyguardPresentFlow: Flow<Boolean> =
             keyguardTransitionInteractor
                 .transitionValue(
@@ -265,11 +258,9 @@
     }
 
     private fun unseenFeatureEnabled(): Flow<Boolean> {
-        if (
-            NotificationMinimalismPrototype.V1.isEnabled ||
-                NotificationMinimalismPrototype.V2.isEnabled
-        ) {
-            return flowOf(true)
+        if (NotificationMinimalismPrototype.isEnabled) {
+            // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
+            return flowOf(false)
         }
         return secureSettings
             // emit whenever the setting has changed
@@ -340,110 +331,18 @@
             }
         }
 
-    private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
-        // Only ever elevate a top unseen notification on keyguard, not even locked shade
-        if (statusBarStateController.state != StatusBarState.KEYGUARD) {
-            seenNotificationsInteractor.setTopOngoingNotification(null)
-            seenNotificationsInteractor.setTopUnseenNotification(null)
-            return
-        }
-        // On keyguard pick the top-ranked unseen or ongoing notification to elevate
-        val nonSummaryEntries: Sequence<NotificationEntry> =
-            list
-                .asSequence()
-                .flatMap {
-                    when (it) {
-                        is NotificationEntry -> listOfNotNull(it)
-                        is GroupEntry -> it.children
-                        else -> error("unhandled type of $it")
-                    }
-                }
-                .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
-        seenNotificationsInteractor.setTopOngoingNotification(
-            nonSummaryEntries
-                .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
-                .minByOrNull { it.ranking.rank }
-        )
-        seenNotificationsInteractor.setTopUnseenNotification(
-            nonSummaryEntries
-                .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
-                .minByOrNull { it.ranking.rank }
-        )
-    }
-
-    @VisibleForTesting
-    val unseenNotifPromoter =
-        object : NotifPromoter("$TAG-unseen") {
-            override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
-                else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false
-                else
-                    seenNotificationsInteractor.isTopOngoingNotification(child) ||
-                        seenNotificationsInteractor.isTopUnseenNotification(child)
-        }
-
-    val topOngoingSectioner =
-        object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
-            override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
-                return entry.anyEntry { notificationEntry ->
-                    seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
-                }
-            }
-        }
-
-    val topUnseenSectioner =
-        object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
-            override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
-                return entry.anyEntry { notificationEntry ->
-                    seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
-                }
-            }
-        }
-
-    private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
-        when {
-            predicate(representativeEntry) -> true
-            this !is GroupEntry -> false
-            else -> children.any(predicate)
-        }
-
     @VisibleForTesting
     val unseenNotifFilter =
-        object : NotifFilter("$TAG-unseen") {
+        object : NotifFilter(TAG) {
 
             var hasFilteredAnyNotifs = false
 
-            /**
-             * Encapsulates a definition of "being on the keyguard". Note that these two definitions
-             * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does
-             * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing]
-             * is any state where the keyguard has not been dismissed, including locked shade and
-             * occluded lock screen.
-             *
-             * Returning false for locked shade and occluded states means that this filter will
-             * allow seen notifications to appear in the locked shade.
-             */
-            private fun isOnKeyguard(): Boolean =
-                if (NotificationMinimalismPrototype.V2.isEnabled) {
-                    false // disable this feature under this prototype
-                } else if (
-                    NotificationMinimalismPrototype.V1.isEnabled &&
-                        NotificationMinimalismPrototype.V1.showOnLockedShade
-                ) {
-                    statusBarStateController.state == StatusBarState.KEYGUARD
-                } else {
-                    keyguardRepository.isKeyguardShowing()
-                }
-
             override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
                 when {
                     // Don't apply filter if the setting is disabled
                     !unseenFilterEnabled -> false
                     // Don't apply filter if the keyguard isn't currently showing
-                    !isOnKeyguard() -> false
+                    !keyguardRepository.isKeyguardShowing() -> false
                     // Don't apply the filter if the notification is unseen
                     unseenNotifications.contains(entry) -> false
                     // Don't apply the filter to (non-promoted) group summaries
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index caa6c17..71c98b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -150,8 +150,9 @@
                     if (entry == null) {
                         return false;
                     }
-                    boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled()
-                            && mSeenNotificationsInteractor.isTopUnseenNotification(entry);
+                    boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+                            && (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
+                                || mSeenNotificationsInteractor.isTopOngoingNotification(entry));
                     if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
                         return !mVisibilityLocationProvider.isInVisibleLocation(entry);
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt
index 5adf31b..0c7ba15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt
@@ -13,12 +13,18 @@
     /** The subset of active listeners which are temporary (will be removed after called) */
     private val temporaryListeners = ArraySet<OnReorderingAllowedListener>()
 
+    private val banListeners = ListenerSet<OnReorderingBannedListener>()
+
     var isReorderingAllowed = true
         set(value) {
             if (field != value) {
                 field = value
                 if (value) {
                     notifyReorderingAllowed()
+                } else {
+                    banListeners.forEach { listener ->
+                        listener.onReorderingBanned()
+                    }
                 }
             }
         }
@@ -38,6 +44,10 @@
         allListeners.addIfAbsent(listener)
     }
 
+    fun addPersistentReorderingBannedListener(listener: OnReorderingBannedListener) {
+        banListeners.addIfAbsent(listener)
+    }
+
     /** Add a listener which will be removed when it is called. */
     fun addTemporaryReorderingAllowedListener(listener: OnReorderingAllowedListener) {
         // Only add to the temporary set if it was added to the global set
@@ -57,3 +67,7 @@
 fun interface OnReorderingAllowedListener {
     fun onReorderingAllowed()
 }
+
+fun interface OnReorderingBannedListener {
+    fun onReorderingBanned()
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index bf44b9f..24b75d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -30,6 +30,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -44,8 +45,17 @@
     private val shadeInteractor: ShadeInteractor,
 ) {
 
+    /** The top-ranked heads up row, regardless of pinned state */
     val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow
 
+    /** The top-ranked heads up row, if that row is pinned */
+    val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> =
+        headsUpRepository.topHeadsUpRow
+            .flatMapLatest { repository ->
+                repository?.isPinned?.map { pinned -> repository.takeIf { pinned } } ?: flowOf(null)
+            }
+            .distinctUntilChanged()
+
     /** Set of currently pinned top-level heads up rows to be displayed. */
     val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
         if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
@@ -89,10 +99,10 @@
             flowOf(false)
         } else {
             combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
-                    hasPinnedRows,
-                    animatingAway ->
-                    hasPinnedRows || animatingAway
-                }
+                hasPinnedRows,
+                animatingAway ->
+                hasPinnedRows || animatingAway
+            }
         }
     }
 
@@ -127,6 +137,9 @@
 
     fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
 
+    /** Returns the Notification Key (the standard string) of this row. */
+    fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key
+
     fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
         headsUpRepository.setHeadsUpAnimatingAway(animatingAway)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 85c66bd..948a3c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.util.IndentingPrintWriter
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.util.printSection
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
 
@@ -41,24 +43,42 @@
 
     /** Set the entry that is identified as the top ongoing notification. */
     fun setTopOngoingNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topOngoingNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top ongoing notification. */
     fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
         else
             entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
 
     /** Set the entry that is identified as the top unseen notification. */
     fun setTopUnseenNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topUnseenNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top unseen notification. */
     fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
         else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
+
+    fun dump(pw: IndentingPrintWriter) =
+        with(pw) {
+            printSection("SeenNotificationsInteractor") {
+                print(
+                    "hasFilteredOutSeenNotifications",
+                    notificationListRepository.hasFilteredOutSeenNotifications.value
+                )
+                print(
+                    "topOngoingNotificationKey",
+                    notificationListRepository.topOngoingNotificationKey.value
+                )
+                print(
+                    "topUnseenNotificationKey",
+                    notificationListRepository.topUnseenNotificationKey.value
+                )
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
index b8af369..fe86375 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -122,12 +122,15 @@
                 val timeRemaining = parseTimeDelta(remaining)
                 TimerContentModel(
                     icon = icon,
-                    name = total,
+                    // TODO: b/352142761 - define and use a string resource rather than " Timer".
+                    // (The UX isn't final so using " Timer" for now).
+                    name = total.replace("Σ", "") + " Timer",
                     state =
                         TimerContentModel.TimerState.Paused(
                             timeRemaining = timeRemaining,
-                            resumeIntent = notification.findActionWithName("Resume"),
-                            resetIntent = notification.findActionWithName("Reset"),
+                            resumeIntent = notification.findStartIntent(),
+                            addMinuteAction = notification.findAddMinuteAction(),
+                            resetAction = notification.findResetAction(),
                         )
                 )
             }
@@ -136,12 +139,15 @@
                 val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
                 TimerContentModel(
                     icon = icon,
-                    name = total,
+                    // TODO: b/352142761 - define and use a string resource rather than " Timer".
+                    // (The UX isn't final so using " Timer" for now).
+                    name = total.replace("Σ", "") + " Timer",
                     state =
                         TimerContentModel.TimerState.Running(
                             finishTime = finishTime,
-                            pauseIntent = notification.findActionWithName("Pause"),
-                            addOneMinuteIntent = notification.findActionWithName("Add 1 min"),
+                            pauseIntent = notification.findPauseIntent(),
+                            addMinuteAction = notification.findAddMinuteAction(),
+                            resetAction = notification.findResetAction(),
                         )
                 )
             }
@@ -149,8 +155,34 @@
         }
     }
 
-    private fun Notification.findActionWithName(name: String): PendingIntent? {
-        return actions.firstOrNull { name == it.title?.toString() }?.actionIntent
+    private fun Notification.findPauseIntent(): PendingIntent? {
+        return actions
+            .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true }
+            ?.actionIntent
+    }
+
+    private fun Notification.findStartIntent(): PendingIntent? {
+        return actions
+            .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true }
+            ?.actionIntent
+    }
+
+    // TODO: b/352142761 - switch to system attributes for label and icon.
+    //   - We probably want a consistent look for the Reset button. (Double check with UX.)
+    //   - Using the custom assets now since I couldn't an existing "Reset" icon.
+    private fun Notification.findResetAction(): Notification.Action? {
+        return actions.firstOrNull {
+            it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true
+        }
+    }
+
+    // TODO: b/352142761 - check with UX on whether this should be required.
+    //   - Alternative is to allow for optional actions in addition to main and reset.
+    //   - For optional actions, we should take the custom label and icon.
+    private fun Notification.findAddMinuteAction(): Notification.Action? {
+        return actions.firstOrNull {
+            it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true
+        }
     }
 
     private fun parseCurrentTime(current: String): Long {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
index 5584701..33b2564 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.row.shared
 
+import android.app.Notification
 import android.app.PendingIntent
 import java.time.Duration
 
@@ -32,6 +33,9 @@
 ) : RichOngoingContentModel {
     /** The state (paused or running) of the timer, and relevant time */
     sealed interface TimerState {
+        val addMinuteAction: Notification.Action?
+        val resetAction: Notification.Action?
+
         /**
          * Indicates a running timer
          *
@@ -41,7 +45,8 @@
         data class Running(
             val finishTime: Long,
             val pauseIntent: PendingIntent?,
-            val addOneMinuteIntent: PendingIntent?,
+            override val addMinuteAction: Notification.Action?,
+            override val resetAction: Notification.Action?,
         ) : TimerState
 
         /**
@@ -53,7 +58,8 @@
         data class Paused(
             val timeRemaining: Duration,
             val resumeIntent: PendingIntent?,
-            val resetIntent: PendingIntent?,
+            override val addMinuteAction: Notification.Action?,
+            override val resetAction: Notification.Action?,
         ) : TimerState
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
index 0d83ace..8c95187 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
@@ -18,8 +18,9 @@
 
 import android.annotation.DrawableRes
 import android.content.Context
+import android.graphics.BlendMode
 import android.util.AttributeSet
-import android.widget.Button
+import com.android.internal.widget.EmphasizedNotificationButton
 
 class TimerButtonView
 @JvmOverloads
@@ -28,14 +29,19 @@
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0,
-) : Button(context, attrs, defStyleAttr, defStyleRes) {
+) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) {
 
     private val Int.dp: Int
         get() = (this * context.resources.displayMetrics.density).toInt()
 
     fun setIcon(@DrawableRes icon: Int) {
         val drawable = context.getDrawable(icon)
+
+        drawable?.mutate()
+        drawable?.setTintList(textColors)
+        drawable?.setTintBlendMode(BlendMode.SRC_IN)
         drawable?.setBounds(0, 0, 24.dp, 24.dp)
+
         setCompoundDrawablesRelative(drawable, null, null, null)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
index 2e164d6..d481b50 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.notification.row.ui.view
 
 import android.content.Context
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
 import android.os.SystemClock
 import android.util.AttributeSet
 import android.widget.Chronometer
@@ -48,6 +48,9 @@
     lateinit var altButton: TimerButtonView
         private set
 
+    lateinit var resetButton: TimerButtonView
+        private set
+
     override fun onFinishInflate() {
         super.onFinishInflate()
         icon = requireViewById(R.id.icon)
@@ -56,13 +59,14 @@
         pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining)
         mainButton = requireViewById(R.id.mainButton)
         altButton = requireViewById(R.id.altButton)
+        resetButton = requireViewById(R.id.resetButton)
     }
 
     /** the resources configuration has changed such that the view needs to be reinflated */
     fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
 
-    fun setIcon(iconDrawable: Drawable?) {
-        this.icon.setImageDrawable(iconDrawable)
+    fun setIcon(icon: Icon?) {
+        this.icon.setImageIcon(icon)
     }
 
     fun setLabel(label: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
index c9ff589..042d1bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.row.ui.viewbinder
 
+import android.content.res.ColorStateList
+import android.graphics.drawable.Icon
 import android.view.View
 import androidx.core.view.isGone
 import androidx.lifecycle.lifecycleScope
@@ -46,12 +48,43 @@
         launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } }
         launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } }
         launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } }
+        launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } }
     }
 
     fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) {
         if (model != null) {
-            buttonView.setIcon(model.iconRes)
-            buttonView.setText(model.labelRes)
+            buttonView.setButtonBackground(
+                ColorStateList.valueOf(
+                    buttonView.context.getColor(com.android.internal.R.color.system_accent2_100)
+                )
+            )
+            buttonView.setTextColor(
+                buttonView.context.getColor(
+                    com.android.internal.R.color.notification_primary_text_color_light
+                )
+            )
+
+            when (model) {
+                is TimerViewModel.ButtonViewModel.WithSystemAttrs -> {
+                    buttonView.setIcon(model.iconRes)
+                    buttonView.setText(model.labelRes)
+                }
+                is TimerViewModel.ButtonViewModel.WithCustomAttrs -> {
+                    // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons
+                    // with empty resPackage? RemoteViews handles this by using a  different
+                    // `contextForResources` for inflation.
+                    val icon =
+                        if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "")
+                            Icon.createWithResource(
+                                "com.google.android.deskclock",
+                                model.icon.resId
+                            )
+                        else model.icon
+                    buttonView.setImageIcon(icon)
+                    buttonView.text = model.label
+                }
+            }
+
             buttonView.setOnClickListener(
                 model.pendingIntent?.let { pendingIntent ->
                     View.OnClickListener { pendingIntent.send() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
index a85c87f..768a093 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
@@ -19,7 +19,7 @@
 import android.annotation.DrawableRes
 import android.annotation.StringRes
 import android.app.PendingIntent
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
 import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
@@ -44,7 +44,7 @@
 
     private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state }
 
-    val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable }
+    val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon }
 
     val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name }
 
@@ -57,13 +57,13 @@
         state.map {
             when (it) {
                 is TimerState.Paused ->
-                    ButtonViewModel(
+                    ButtonViewModel.WithSystemAttrs(
                         it.resumeIntent,
                         com.android.systemui.res.R.string.controls_media_resume, // "Resume",
                         com.android.systemui.res.R.drawable.ic_media_play
                     )
                 is TimerState.Running ->
-                    ButtonViewModel(
+                    ButtonViewModel.WithSystemAttrs(
                         it.pauseIntent,
                         com.android.systemui.res.R.string.controls_media_button_pause, // "Pause",
                         com.android.systemui.res.R.drawable.ic_media_pause
@@ -73,31 +73,41 @@
 
     val altButtonModel: Flow<ButtonViewModel?> =
         state.map {
-            when (it) {
-                is TimerState.Paused ->
-                    it.resetIntent?.let { resetIntent ->
-                        ButtonViewModel(
-                            resetIntent,
-                            com.android.systemui.res.R.string.reset, // "Reset",
-                            com.android.systemui.res.R.drawable.ic_close_white_rounded
-                        )
-                    }
-                is TimerState.Running ->
-                    it.addOneMinuteIntent?.let { addOneMinuteIntent ->
-                        ButtonViewModel(
-                            addOneMinuteIntent,
-                            com.android.systemui.res.R.string.add, // "Add 1 minute",
-                            com.android.systemui.res.R.drawable.ic_add
-                        )
-                    }
+            it.addMinuteAction?.let { action ->
+                ButtonViewModel.WithCustomAttrs(
+                    action.actionIntent,
+                    action.title, // "1:00",
+                    action.getIcon()
+                )
             }
         }
 
-    data class ButtonViewModel(
-        val pendingIntent: PendingIntent?,
-        @StringRes val labelRes: Int,
-        @DrawableRes val iconRes: Int,
-    )
+    val resetButtonModel: Flow<ButtonViewModel?> =
+        state.map {
+            it.resetAction?.let { action ->
+                ButtonViewModel.WithCustomAttrs(
+                    action.actionIntent,
+                    action.title, // "Reset",
+                    action.getIcon()
+                )
+            }
+        }
+
+    sealed interface ButtonViewModel {
+        val pendingIntent: PendingIntent?
+
+        data class WithSystemAttrs(
+            override val pendingIntent: PendingIntent?,
+            @StringRes val labelRes: Int,
+            @DrawableRes val iconRes: Int,
+        ) : ButtonViewModel
+
+        data class WithCustomAttrs(
+            override val pendingIntent: PendingIntent?,
+            val label: CharSequence,
+            val icon: Icon,
+        ) : ButtonViewModel
+    }
 }
 
 private fun Duration.format(): String {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
index bf37036..06f3db5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
@@ -24,102 +24,43 @@
 /** Helper for reading or using the minimalism prototype flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NotificationMinimalismPrototype {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
 
-    val version: Int by lazy {
-        SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2)
-    }
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
 
-    object V1 {
-        /** The aconfig flag name */
-        const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+    /** Is the heads-up cycling animation enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationMinimalismPrototype()
 
-        /** A token used for dependency declaration */
-        val token: FlagToken
-            get() = FlagToken(FLAG_NAME, isEnabled)
+    /**
+     * The prototype will (by default) use a promoter to ensure that the top unseen notification is
+     * not grouped, but this property read allows that behavior to be disabled.
+     */
+    val ungroupTopUnseen: Boolean
+        get() =
+            if (isUnexpectedlyInLegacyMode()) false
+            else
+                SystemProperties.getBoolean(
+                    "persist.notification_minimalism_prototype.ungroup_top_unseen",
+                    false
+                )
 
-        /** Is the heads-up cycling animation enabled */
-        @JvmStatic
-        inline val isEnabled
-            get() = Flags.notificationMinimalismPrototype() && version == 1
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
 
-        /**
-         * the prototype will now show seen notifications on the locked shade by default, but this
-         * property read allows that to be quickly disabled for testing
-         */
-        val showOnLockedShade: Boolean
-            get() =
-                if (isUnexpectedlyInLegacyMode()) false
-                else
-                    SystemProperties.getBoolean(
-                        "persist.notification_minimalism_prototype.show_on_locked_shade",
-                        true
-                    )
-
-        /** gets the configurable max number of notifications */
-        val maxNotifs: Int
-            get() =
-                if (isUnexpectedlyInLegacyMode()) -1
-                else
-                    SystemProperties.getInt(
-                        "persist.notification_minimalism_prototype.lock_screen_max_notifs",
-                        1
-                    )
-
-        /**
-         * Called to ensure code is only run when the flag is enabled. This protects users from the
-         * unintended behaviors caused by accidentally running new logic, while also crashing on an
-         * eng build to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun isUnexpectedlyInLegacyMode() =
-            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-        /**
-         * Called to ensure code is only run when the flag is disabled. This will throw an exception
-         * if the flag is enabled to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-    }
-    object V2 {
-        const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
-
-        /** A token used for dependency declaration */
-        val token: FlagToken
-            get() = FlagToken(FLAG_NAME, isEnabled)
-
-        /** Is the heads-up cycling animation enabled */
-        @JvmStatic
-        inline val isEnabled
-            get() = Flags.notificationMinimalismPrototype() && version == 2
-
-        /**
-         * The prototype will (by default) use a promoter to ensure that the top unseen notification
-         * is not grouped, but this property read allows that behavior to be disabled.
-         */
-        val ungroupTopUnseen: Boolean
-            get() =
-                if (isUnexpectedlyInLegacyMode()) false
-                else
-                    SystemProperties.getBoolean(
-                        "persist.notification_minimalism_prototype.ungroup_top_unseen",
-                        true
-                    )
-
-        /**
-         * Called to ensure code is only run when the flag is enabled. This protects users from the
-         * unintended behaviors caused by accidentally running new logic, while also crashing on an
-         * eng build to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun isUnexpectedlyInLegacyMode() =
-            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-        /**
-         * Called to ensure code is only run when the flag is disabled. This will throw an exception
-         * if the flag is enabled to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-    }
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
 }
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 cec1ef3..4a447b7 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
@@ -4862,14 +4862,20 @@
      * @param isHeadsUp true for appear, false for disappear animations
      */
     public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
-        final boolean add = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed);
+        final boolean closedAndSeenInShade = !mIsExpanded && row.getEntry() != null
+                && row.getEntry().isSeenInShade();
+        final boolean addAnimation = mAnimationsEnabled && !closedAndSeenInShade &&
+                (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed);
         if (SPEW) {
             Log.v(TAG, "generateHeadsUpAnimation:"
-                    + " willAdd=" + add
-                    + " isHeadsUp=" + isHeadsUp
-                    + " row=" + row.getEntry().getKey());
+                    + " addAnimation=" + addAnimation
+                    + (row.getEntry() == null ? " entry NULL "
+                            : " isSeenInShade=" + row.getEntry().isSeenInShade()
+                                    + " row=" + row.getEntry().getKey())
+                    + " mIsExpanded=" + mIsExpanded
+                    + " isHeadsUp=" + isHeadsUp);
         }
-        if (add) {
+        if (addAnimation) {
             // If we're hiding a HUN we just started showing THIS FRAME, then remove that event,
             // and do not add the disappear event either.
             if (!isHeadsUp && mHeadsUpChangeAnimations.remove(new Pair<>(row, true))) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 391bc43..06222fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -74,7 +74,7 @@
 
     /** Whether we allow keyguard to show less important notifications above the shelf. */
     private val limitLockScreenToOneImportant
-        get() = NotificationMinimalismPrototype.V2.isEnabled
+        get() = NotificationMinimalismPrototype.isEnabled
 
     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
     private var dividerHeight by notNull<Float>()
@@ -405,16 +405,8 @@
 
     fun updateResources() {
         maxKeyguardNotifications =
-            infiniteIfNegative(
-                if (NotificationMinimalismPrototype.V1.isEnabled) {
-                    NotificationMinimalismPrototype.V1.maxNotifs
-                } else {
-                    resources.getInteger(R.integer.keyguard_max_notification_count)
-                }
-            )
-        maxNotificationsExcludesMedia =
-            NotificationMinimalismPrototype.V1.isEnabled ||
-                NotificationMinimalismPrototype.V2.isEnabled
+            infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+        maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
 
         dividerHeight =
             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 97266c5..86c7c6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Lazy
@@ -36,10 +37,16 @@
 constructor(
     private val statusBarStateController: SysuiStatusBarStateController,
     @Main private val mainExecutor: DelayableExecutor,
+    activityStarterInternal: Lazy<ActivityStarterInternalImpl>,
     legacyActivityStarter: Lazy<LegacyActivityStarterInternalImpl>
 ) : ActivityStarter {
 
-    private val activityStarterInternal: ActivityStarterInternal = legacyActivityStarter.get()
+    private val activityStarterInternal: ActivityStarterInternal =
+        if (SceneContainerFlag.isEnabled) {
+            activityStarterInternal.get()
+        } else {
+            legacyActivityStarter.get()
+        }
 
     override fun startPendingIntentDismissingKeyguard(intent: PendingIntent) {
         activityStarterInternal.startPendingIntentDismissingKeyguard(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
index ae98e1d..107bf1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -16,23 +16,93 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.ActivityTaskManager
 import android.app.PendingIntent
+import android.app.TaskStackBuilder
+import android.content.Context
 import android.content.Intent
+import android.content.res.Resources
 import android.os.Bundle
+import android.os.RemoteException
 import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.view.RemoteAnimationAdapter
 import android.view.View
+import android.view.WindowManager
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.Flags
 import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DelegateTransitionAnimatorController
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.camera.CameraIntents
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.DisplayId
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.kotlin.getOrNull
+import dagger.Lazy
+import java.util.Optional
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /**
- * Encapsulates the activity logic for activity starter when flexiglass is enabled.
+ * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled.
  *
  * TODO: b/308819693
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
-class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInternal {
+class ActivityStarterInternalImpl
+@Inject
+constructor(
+    private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>,
+    private val context: Context,
+    @Main private val resources: Resources,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val activityTransitionAnimator: ActivityTransitionAnimator,
+    @DisplayId private val displayId: Int,
+    private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
+    private val activityIntentHelper: ActivityIntentHelper,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val assistManagerLazy: Lazy<AssistManager>,
+    @Main private val mainExecutor: DelayableExecutor,
+    private val shadeControllerLazy: Lazy<ShadeController>,
+    private val communalSceneInteractor: CommunalSceneInteractor,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
+    private val shadeAnimationInteractor: ShadeAnimationInteractor,
+    private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
+    private val commandQueue: CommandQueue,
+    private val lockScreenUserManager: NotificationLockscreenUserManager,
+) : ActivityStarterInternal {
+    private val centralSurfaces: CentralSurfaces?
+        get() = centralSurfacesOptLazy.get().getOrNull()
+
     override fun startPendingIntentDismissingKeyguard(
         intent: PendingIntent,
         dismissShade: Boolean,
@@ -45,7 +115,119 @@
         extraOptions: Bundle?,
         customMessage: String?,
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val animationController =
+            if (associatedView is ExpandableNotificationRow) {
+                centralSurfaces?.getAnimatorControllerFromNotification(associatedView)
+            } else animationController
+
+        val willLaunchResolverActivity =
+            intent.isActivity &&
+                activityIntentHelper.wouldPendingLaunchResolverActivity(
+                    intent,
+                    lockScreenUserManager.currentUserId,
+                )
+
+        val actuallyShowOverLockscreen =
+            showOverLockscreen &&
+                intent.isActivity &&
+                (skipLockscreenChecks ||
+                    activityIntentHelper.wouldPendingShowOverLockscreen(
+                        intent,
+                        lockScreenUserManager.currentUserId
+                    ))
+
+        val animate =
+            !willLaunchResolverActivity &&
+                animationController != null &&
+                shouldAnimateLaunch(intent.isActivity, actuallyShowOverLockscreen)
+
+        // We wrap animationCallback with a StatusBarLaunchAnimatorController so
+        // that the shade is collapsed after the animation (or when it is cancelled,
+        // aborted, etc).
+        val statusBarController =
+            wrapAnimationControllerForShadeOrStatusBar(
+                animationController = animationController,
+                dismissShade = dismissShade,
+                isLaunchForActivity = intent.isActivity,
+            )
+        val controller =
+            if (actuallyShowOverLockscreen) {
+                wrapAnimationControllerForLockscreen(dismissShade, statusBarController)
+            } else {
+                statusBarController
+            }
+
+        // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we
+        // run the animation on the keyguard). The animation will take care of (instantly)
+        // collapsing the shade and hiding the keyguard once it is done.
+        val collapse = dismissShade && !animate
+        val runnable = Runnable {
+            try {
+                activityTransitionAnimator.startPendingIntentWithAnimation(
+                    controller,
+                    animate,
+                    intent.creatorPackage,
+                    actuallyShowOverLockscreen,
+                    object : ActivityTransitionAnimator.PendingIntentStarter {
+                        override fun startPendingIntent(
+                            animationAdapter: RemoteAnimationAdapter?
+                        ): Int {
+                            val options =
+                                ActivityOptions(
+                                    CentralSurfaces.getActivityOptions(displayId, animationAdapter)
+                                        .apply { extraOptions?.let { putAll(it) } }
+                                )
+                            // TODO b/221255671: restrict this to only be set for notifications
+                            options.isEligibleForLegacyPermissionPrompt = true
+                            options.setPendingIntentBackgroundActivityStartMode(
+                                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+                            )
+                            return intent.sendAndReturnResult(
+                                context,
+                                0,
+                                fillInIntent,
+                                null,
+                                null,
+                                null,
+                                options.toBundle()
+                            )
+                        }
+                    },
+                )
+            } catch (e: PendingIntent.CanceledException) {
+                // the stack trace isn't very helpful here.
+                // Just log the exception message.
+                Log.w(TAG, "Sending intent failed: $e")
+                if (!collapse) {
+                    // executeRunnableDismissingKeyguard did not collapse for us already.
+                    shadeControllerLazy.get().collapseOnMainThread()
+                }
+                // TODO: Dismiss Keyguard.
+            }
+            if (intent.isActivity) {
+                assistManagerLazy.get().hideAssist()
+                // This activity could have started while the device is dreaming, in which case
+                // the dream would occlude the activity. In order to show the newly started
+                // activity, we wake from the dream.
+                centralSurfaces?.awakenDreams()
+            }
+            intentSentUiThreadCallback?.let { mainExecutor.execute(it) }
+        }
+
+        if (!actuallyShowOverLockscreen) {
+            mainExecutor.execute {
+                executeRunnableDismissingKeyguard(
+                    runnable = runnable,
+                    afterKeyguardGone = willLaunchResolverActivity,
+                    dismissShade = collapse,
+                    willAnimateOnKeyguard = animate,
+                    customMessage = customMessage,
+                )
+            }
+        } else {
+            mainExecutor.execute(runnable)
+        }
     }
 
     override fun startActivityDismissingKeyguard(
@@ -59,7 +241,116 @@
         disallowEnterPictureInPictureWhileLaunching: Boolean,
         userHandle: UserHandle?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val userHandle: UserHandle = userHandle ?: getActivityUserHandle(intent)
+
+        if (onlyProvisioned && !deviceProvisioningInteractor.isDeviceProvisioned()) return
+
+        val willLaunchResolverActivity: Boolean =
+            activityIntentHelper.wouldLaunchResolverActivity(
+                intent,
+                selectedUserInteractor.getSelectedUserId(),
+            )
+
+        val animate =
+            animationController != null &&
+                !willLaunchResolverActivity &&
+                shouldAnimateLaunch(isActivityIntent = true)
+        val animController =
+            wrapAnimationControllerForShadeOrStatusBar(
+                animationController = animationController,
+                dismissShade = dismissShade,
+                isLaunchForActivity = true,
+            )
+
+        // If we animate, we will dismiss the shade only once the animation is done. This is
+        // taken care of by the StatusBarLaunchAnimationController.
+        val dismissShadeDirectly = dismissShade && animController == null
+
+        val runnable = Runnable {
+            assistManagerLazy.get().hideAssist()
+            intent.flags =
+                if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT != 0) {
+                    Intent.FLAG_ACTIVITY_NEW_TASK
+                } else {
+                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+                }
+            intent.addFlags(flags)
+            val result = intArrayOf(ActivityManager.START_CANCELED)
+            activityTransitionAnimator.startIntentWithAnimation(
+                animController,
+                animate,
+                intent.getPackage()
+            ) { adapter: RemoteAnimationAdapter? ->
+                val options =
+                    ActivityOptions(CentralSurfaces.getActivityOptions(displayId, adapter))
+
+                // We know that the intent of the caller is to dismiss the keyguard and
+                // this runnable is called right after the keyguard is solved, so we tell
+                // WM that we should dismiss it to avoid flickers when opening an activity
+                // that can also be shown over the keyguard.
+                options.setDismissKeyguardIfInsecure()
+                options.setDisallowEnterPictureInPictureWhileLaunching(
+                    disallowEnterPictureInPictureWhileLaunching
+                )
+                if (CameraIntents.isInsecureCameraIntent(intent)) {
+                    // Normally an activity will set it's requested rotation
+                    // animation on its window. However when launching an activity
+                    // causes the orientation to change this is too late. In these cases
+                    // the default animation is used. This doesn't look good for
+                    // the camera (as it rotates the camera contents out of sync
+                    // with physical reality). So, we ask the WindowManager to
+                    // force the cross fade animation if an orientation change
+                    // happens to occur during the launch.
+                    options.rotationAnimationHint =
+                        WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS
+                }
+                if (Settings.Panel.ACTION_VOLUME == intent.action) {
+                    // Settings Panel is implemented as activity(not a dialog), so
+                    // underlying app is paused and may enter picture-in-picture mode
+                    // as a result.
+                    // So we need to disable picture-in-picture mode here
+                    // if it is volume panel.
+                    options.setDisallowEnterPictureInPictureWhileLaunching(true)
+                }
+                try {
+                    result[0] =
+                        ActivityTaskManager.getService()
+                            .startActivityAsUser(
+                                null,
+                                context.basePackageName,
+                                context.attributionTag,
+                                intent,
+                                intent.resolveTypeIfNeeded(context.contentResolver),
+                                null,
+                                null,
+                                0,
+                                Intent.FLAG_ACTIVITY_NEW_TASK,
+                                null,
+                                options.toBundle(),
+                                userHandle.identifier,
+                            )
+                } catch (e: RemoteException) {
+                    Log.w(TAG, "Unable to start activity", e)
+                }
+                result[0]
+            }
+            callback?.onActivityStarted(result[0])
+        }
+        val cancelRunnable = Runnable {
+            callback?.onActivityStarted(ActivityManager.START_CANCELED)
+        }
+        // Do not deferKeyguard when occluded because, when keyguard is occluded,
+        // we do not launch the activity until keyguard is done.
+        executeRunnableDismissingKeyguard(
+            runnable,
+            cancelRunnable,
+            dismissShadeDirectly,
+            willLaunchResolverActivity,
+            deferred = !isKeyguardOccluded(),
+            animate,
+            customMessage,
+        )
     }
 
     override fun startActivity(
@@ -69,7 +360,64 @@
         showOverLockscreenWhenLocked: Boolean,
         userHandle: UserHandle?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val userHandle = userHandle ?: getActivityUserHandle(intent)
+        // Make sure that we dismiss the keyguard if it is directly dismissible or when we don't
+        // want to show the activity above it.
+        if (deviceEntryInteractor.isUnlocked.value || !showOverLockscreenWhenLocked) {
+            startActivityDismissingKeyguard(
+                intent = intent,
+                onlyProvisioned = false,
+                dismissShade = dismissShade,
+                disallowEnterPictureInPictureWhileLaunching = false,
+                callback = null,
+                flags = 0,
+                animationController = animationController,
+                userHandle = userHandle,
+            )
+            return
+        }
+
+        val animate =
+            animationController != null &&
+                shouldAnimateLaunch(
+                    isActivityIntent = true,
+                    showOverLockscreen = showOverLockscreenWhenLocked
+                )
+
+        var controller: ActivityTransitionAnimator.Controller? = null
+        if (animate) {
+            // Wrap the animation controller to dismiss the shade and set
+            // mIsLaunchingActivityOverLockscreen during the animation.
+            val delegate =
+                wrapAnimationControllerForShadeOrStatusBar(
+                    animationController = animationController,
+                    dismissShade = dismissShade,
+                    isLaunchForActivity = true,
+                )
+            controller = wrapAnimationControllerForLockscreen(dismissShade, delegate)
+        } else if (dismissShade) {
+            // The animation will take care of dismissing the shade at the end of the animation.
+            // If we don't animate, collapse it directly.
+            shadeControllerLazy.get().cancelExpansionAndCollapseShade()
+        }
+
+        // We should exit the dream to prevent the activity from starting below the
+        // dream.
+        if (keyguardInteractor.isDreaming.value) {
+            centralSurfaces?.awakenDreams()
+        }
+
+        activityTransitionAnimator.startIntentWithAnimation(
+            controller,
+            animate,
+            intent.getPackage(),
+            showOverLockscreenWhenLocked
+        ) { adapter: RemoteAnimationAdapter? ->
+            TaskStackBuilder.create(context)
+                .addNextIntent(intent)
+                .startActivities(CentralSurfaces.getActivityOptions(displayId, adapter), userHandle)
+        }
     }
 
     override fun dismissKeyguardThenExecute(
@@ -78,7 +426,23 @@
         afterKeyguardGone: Boolean,
         customMessage: String?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        Log.i(TAG, "Invoking dismissKeyguardThenExecute, afterKeyguardGone: $afterKeyguardGone")
+
+        // TODO b/308819693: startWakeAndUnlock animation when pulsing
+
+        if (isKeyguardShowing()) {
+            statusBarKeyguardViewManagerLazy
+                .get()
+                .dismissWithAction(action, cancel, afterKeyguardGone, customMessage)
+        } else {
+            // If the keyguard isn't showing but the device is dreaming, we should exit the
+            // dream.
+            if (keyguardInteractor.isDreaming.value) {
+                centralSurfaces?.awakenDreams()
+            }
+            action.onDismiss()
+        }
     }
 
     override fun executeRunnableDismissingKeyguard(
@@ -90,10 +454,195 @@
         willAnimateOnKeyguard: Boolean,
         customMessage: String?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val onDismissAction: ActivityStarter.OnDismissAction =
+            object : ActivityStarter.OnDismissAction {
+                override fun onDismiss(): Boolean {
+                    if (runnable != null) {
+                        if (isKeyguardOccluded()) {
+                            statusBarKeyguardViewManagerLazy
+                                .get()
+                                .addAfterKeyguardGoneRunnable(runnable)
+                        } else {
+                            mainExecutor.execute(runnable)
+                        }
+                    }
+                    if (dismissShade) {
+                        shadeControllerLazy.get().collapseShadeForActivityStart()
+                    }
+                    if (Flags.communalHub()) {
+                        communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard()
+                    }
+                    return deferred
+                }
+
+                override fun willRunAnimationOnKeyguard(): Boolean {
+                    if (Flags.communalHub() && communalSceneInteractor.isIdleOnCommunal.value) {
+                        // Override to false when launching activity over the hub that requires auth
+                        return false
+                    }
+                    return willAnimateOnKeyguard
+                }
+            }
+        dismissKeyguardThenExecute(
+            onDismissAction,
+            cancelAction,
+            afterKeyguardGone,
+            customMessage,
+        )
     }
 
     override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean {
-        TODO("Not yet implemented b/308819693")
+        return shouldAnimateLaunch(isActivityIntent, false)
+    }
+
+    /**
+     * Whether we should animate an activity launch.
+     *
+     * Note: This method must be called *before* dismissing the keyguard.
+     */
+    private fun shouldAnimateLaunch(
+        isActivityIntent: Boolean,
+        showOverLockscreen: Boolean,
+    ): Boolean {
+        // TODO(b/294418322): always support launch animations when occluded.
+        val ignoreOcclusion = showOverLockscreen && Flags.mediaLockscreenLaunchAnimation()
+        if (isKeyguardOccluded() && !ignoreOcclusion) {
+            return false
+        }
+
+        // Always animate if we are not showing the keyguard or if we animate over the lockscreen
+        // (without unlocking it).
+        if (showOverLockscreen || !isKeyguardShowing()) {
+            return true
+        }
+
+        // We don't animate non-activity launches as they can break the animation.
+        // TODO(b/184121838): Support non activity launches on the lockscreen.
+        return isActivityIntent
+    }
+
+    /** Retrieves the current user handle to start the Activity. */
+    private fun getActivityUserHandle(intent: Intent): UserHandle {
+        val packages: Array<String> = resources.getStringArray(R.array.system_ui_packages)
+        for (pkg in packages) {
+            val componentName = intent.component ?: break
+            if (pkg == componentName.packageName) {
+                return UserHandle(UserHandle.myUserId())
+            }
+        }
+        return UserHandle(selectedUserInteractor.getSelectedUserId())
+    }
+
+    private fun isKeyguardShowing(): Boolean {
+        return !deviceEntryInteractor.isDeviceEntered.value
+    }
+
+    private fun isKeyguardOccluded(): Boolean {
+        return keyguardTransitionInteractor.getCurrentState() == KeyguardState.OCCLUDED
+    }
+
+    /**
+     * Return a [ActivityTransitionAnimator.Controller] wrapping `animationController` so that:
+     * - if it launches in the notification shade window and `dismissShade` is true, then the shade
+     *   will be instantly dismissed at the end of the animation.
+     * - if it launches in status bar window, it will make the status bar window match the device
+     *   size during the animation (that way, the animation won't be clipped by the status bar
+     *   size).
+     *
+     * @param animationController the controller that is wrapped and will drive the main animation.
+     * @param dismissShade whether the notification shade will be dismissed at the end of the
+     *   animation. This is ignored if `animationController` is not animating in the shade window.
+     * @param isLaunchForActivity whether the launch is for an activity.
+     */
+    private fun wrapAnimationControllerForShadeOrStatusBar(
+        animationController: ActivityTransitionAnimator.Controller?,
+        dismissShade: Boolean,
+        isLaunchForActivity: Boolean,
+    ): ActivityTransitionAnimator.Controller? {
+        if (animationController == null) {
+            return null
+        }
+        val rootView = animationController.transitionContainer.rootView
+        val controllerFromStatusBar: Optional<ActivityTransitionAnimator.Controller> =
+            statusBarWindowController.wrapAnimationControllerIfInStatusBar(
+                rootView,
+                animationController
+            )
+        if (controllerFromStatusBar.isPresent) {
+            return controllerFromStatusBar.get()
+        }
+
+        centralSurfaces?.let {
+            // If the view is not in the status bar, then we are animating a view in the shade.
+            // We have to make sure that we collapse it when the animation ends or is cancelled.
+            if (dismissShade) {
+                return StatusBarTransitionAnimatorController(
+                    animationController,
+                    shadeAnimationInteractor,
+                    shadeControllerLazy.get(),
+                    notifShadeWindowControllerLazy.get(),
+                    commandQueue,
+                    displayId,
+                    isLaunchForActivity
+                )
+            }
+        }
+
+        return animationController
+    }
+
+    /**
+     * Wraps an animation controller so that if an activity would be launched on top of the
+     * lockscreen, the correct flags are set for it to be occluded.
+     */
+    private fun wrapAnimationControllerForLockscreen(
+        dismissShade: Boolean,
+        animationController: ActivityTransitionAnimator.Controller?
+    ): ActivityTransitionAnimator.Controller? {
+        return animationController?.let {
+            object : DelegateTransitionAnimatorController(it) {
+                override fun onIntentStarted(willAnimate: Boolean) {
+                    delegate.onIntentStarted(willAnimate)
+                    if (willAnimate) {
+                        centralSurfaces?.setIsLaunchingActivityOverLockscreen(true, dismissShade)
+                    }
+                }
+
+                override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+                    super.onTransitionAnimationStart(isExpandingFullyAbove)
+                    if (Flags.communalHub()) {
+                        communalSceneInteractor.snapToScene(
+                            CommunalScenes.Blank,
+                            ActivityTransitionAnimator.TIMINGS.totalDuration
+                        )
+                    }
+                }
+
+                override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually
+                    // finishing the animation so that we can assume that
+                    // mIsLaunchingActivityOverLockscreen being true means that we will
+                    // collapse the shade (or at least run the post collapse runnables)
+                    // later on.
+                    centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+                    delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
+                }
+
+                override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually
+                    // finishing the animation so that we can assume that
+                    // mIsLaunchingActivityOverLockscreen being true means that we will
+                    // collapse the shade (or at least run the // post collapse
+                    // runnables) later on.
+                    centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+                    delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "ActivityStarterInternalImpl"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index a2e44df..8577d48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -39,6 +39,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
+import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
@@ -86,7 +87,7 @@
     private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
     private final VisualStabilityProvider mVisualStabilityProvider;
 
-    private final AvalancheController mAvalancheController;
+    private AvalancheController mAvalancheController;
 
     // TODO(b/328393698) move the topHeadsUpRow logic to an interactor
     private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow =
@@ -173,6 +174,9 @@
         });
         javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
                     this::onShadeOrQsExpanded);
+        mVisualStabilityProvider.addPersistentReorderingBannedListener(mOnReorderingBannedListener);
+        mVisualStabilityProvider.addPersistentReorderingAllowedListener(
+                mOnReorderingAllowedListener);
     }
 
     public void setAnimationStateHandler(AnimationStateHandler handler) {
@@ -379,6 +383,7 @@
 
     private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
+        mAvalancheController.setEnableAtRuntime(true);
         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
             if (isHeadsUpEntry(entry.getKey())) {
                 // Maybe the heads-up was removed already
@@ -389,6 +394,22 @@
         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
     };
 
+    private final OnReorderingBannedListener mOnReorderingBannedListener = () -> {
+        if (mAvalancheController != null) {
+            // In open shade the first HUN is pinned, and visual stability logic prevents us from
+            // unpinning this first HUN as long as the shade remains open. AvalancheController only
+            // shows the next HUN when the currently showing HUN is unpinned, so we must disable
+            // throttling here so that the incoming HUN stream is not forever paused. This is reset
+            // when reorder becomes allowed.
+            mAvalancheController.setEnableAtRuntime(false);
+
+            // Note that we cannot do the above when
+            // 1) The remove runnable runs because its delay means it may not run before shade close
+            // 2) Reordering is allowed again (when shade closes) because the HUN appear animation
+            // will have started by then
+        }
+    };
+
     ///////////////////////////////////////////////////////////////////////////////////////////////
     //  HeadsUpManager utility (protected) methods overrides:
 
@@ -561,18 +582,26 @@
         }
 
         @Override
+        protected void setEntry(@androidx.annotation.NonNull NotificationEntry entry,
+                @androidx.annotation.Nullable Runnable removeRunnable) {
+            super.setEntry(entry, removeRunnable);
+
+            if (!mVisualStabilityProvider.isReorderingAllowed()
+                    // We don't want to allow reordering while pulsing, but headsup need to
+                    // time out anyway
+                    && !entry.showingPulsing()) {
+                mEntriesToRemoveWhenReorderingAllowed.add(entry);
+                entry.setSeenInShade(true);
+            }
+        }
+
+        @Override
         protected Runnable createRemoveRunnable(NotificationEntry entry) {
-            return  () -> {
-                if (!mVisualStabilityProvider.isReorderingAllowed()
-                        // We don't want to allow reordering while pulsing, but headsup need to
-                        // time out anyway
-                        && !entry.showingPulsing()) {
-                    mEntriesToRemoveWhenReorderingAllowed.add(entry);
-                    mVisualStabilityProvider.addTemporaryReorderingAllowedListener(
-                            mOnReorderingAllowedListener);
-                } else if (mTrackingHeadsUp) {
+            return () -> {
+                if (mTrackingHeadsUp) {
                     mEntriesToRemoveAfterExpand.add(entry);
-                } else {
+                } else if (mVisualStabilityProvider.isReorderingAllowed()
+                        || entry.showingPulsing()) {
                     removeEntry(entry.getKey(), "createRemoveRunnable");
                 }
             };
@@ -585,9 +614,6 @@
             if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
                 mEntriesToRemoveAfterExpand.remove(mEntry);
             }
-            if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
-                mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
-            }
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index 4c97854..16bd7f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -110,7 +110,7 @@
                                     chipView.setOnClickListener(chipModel.onClickListener)
 
                                     // Accessibility
-                                    setChipAccessibility(chipModel, chipView)
+                                    setChipAccessibility(chipModel, chipView, chipBackgroundView)
 
                                     // Colors
                                     val textColor = chipModel.colors.text(chipContext)
@@ -213,7 +213,11 @@
         this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom)
     }
 
-    private fun setChipAccessibility(chipModel: OngoingActivityChipModel.Shown, chipView: View) {
+    private fun setChipAccessibility(
+        chipModel: OngoingActivityChipModel.Shown,
+        chipView: View,
+        chipBackgroundView: View,
+    ) {
         when (chipModel) {
             is OngoingActivityChipModel.Shown.Countdown -> {
                 // Set as assertive so talkback will announce the countdown
@@ -224,6 +228,16 @@
                 chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE
             }
         }
+        // Clickable chips need to be a minimum size for accessibility purposes, but let
+        // non-clickable chips be smaller.
+        if (chipModel.onClickListener != null) {
+            chipBackgroundView.minimumWidth =
+                chipBackgroundView.context.resources.getDimensionPixelSize(
+                    R.dimen.min_clickable_item_size
+                )
+        } else {
+            chipBackgroundView.minimumWidth = 0
+        }
     }
 
     private fun animateLightsOutView(view: View, visible: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 40799583..6aaa948 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -44,6 +44,16 @@
 
     private val tag = "AvalancheController"
     private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
+    var enableAtRuntime = true
+        set(value) {
+            if (!value) {
+                // Waiting HUNs in AvalancheController are shown in the HUN section in open shade.
+                // Clear them so we don't show them again when the shade closes and reordering is
+                // allowed again.
+                logDroppedHunsInBackground(getWaitingKeys().size)
+                clearNext()
+            }
+        }
 
     // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD
     @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null
@@ -90,6 +100,10 @@
         return getKey(headsUpEntryShowing)
     }
 
+    fun isEnabled() : Boolean {
+        return NotificationThrottleHun.isEnabled && enableAtRuntime
+    }
+
     /** Run or delay Runnable for given HeadsUpEntry */
     fun update(entry: HeadsUpEntry?, runnable: Runnable?, label: String) {
         if (runnable == null) {
@@ -185,7 +199,8 @@
             showNext()
             runnable.run()
         } else {
-            log { "$fn => removing untracked ${getKey(entry)}" }
+            log { "$fn => run runnable for untracked shown ${getKey(entry)}" }
+            runnable.run()
         }
         logState("after $fn")
     }
@@ -197,7 +212,7 @@
      *    BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration.
      */
     fun getDurationMs(entry: HeadsUpEntry, autoDismissMs: Int): Int {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             // Use default duration, like we did before AvalancheController existed
             return autoDismissMs
         }
@@ -246,7 +261,7 @@
 
     /** Return true if entry is waiting to show. */
     fun isWaiting(key: String): Boolean {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return false
         }
         for (entry in nextMap.keys) {
@@ -259,7 +274,7 @@
 
     /** Return list of keys for huns waiting */
     fun getWaitingKeys(): MutableList<String> {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return mutableListOf()
         }
         val keyList = mutableListOf<String>()
@@ -270,7 +285,7 @@
     }
 
     fun getWaitingEntry(key: String): HeadsUpEntry? {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return null
         }
         for (headsUpEntry in nextMap.keys) {
@@ -282,7 +297,7 @@
     }
 
     fun getWaitingEntryList(): List<HeadsUpEntry> {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return mutableListOf()
         }
         return nextMap.keys.toList()
@@ -340,7 +355,7 @@
         showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList)
     }
 
-    fun logDroppedHunsInBackground(numDropped: Int) {
+    private fun logDroppedHunsInBackground(numDropped: Int) {
         bgHandler.post(Runnable {
             // Do this in the background to avoid missing frames when closing the shade
             for (n in 1..numDropped) {
@@ -367,7 +382,8 @@
             "\nPREVIOUS: [$previousHunKey]" +
             "\nNEXT LIST: $nextListStr" +
             "\nNEXT MAP: $nextMapStr" +
-            "\nDROPPED: $dropSetStr"
+            "\nDROPPED: $dropSetStr" +
+            "\nENABLED: $enableAtRuntime"
     }
 
     private fun logState(reason: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 220e729..a0eb989 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -756,7 +756,7 @@
             setEntry(entry, createRemoveRunnable(entry));
         }
 
-        private void setEntry(@NonNull final NotificationEntry entry,
+        protected void setEntry(@NonNull final NotificationEntry entry,
                 @Nullable Runnable removeRunnable) {
             mEntry = entry;
             mRemoveRunnable = removeRunnable;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
index 4838554..07bbca7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
@@ -31,6 +31,13 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean>
+
+    /**
+     * Whether this device has been provisioned.
+     *
+     * @see android.provider.Settings.Global.DEVICE_PROVISIONED
+     */
+    fun isDeviceProvisioned(): Boolean
 }
 
 @Module
@@ -48,11 +55,15 @@
         val listener =
             object : DeviceProvisionedController.DeviceProvisionedListener {
                 override fun onDeviceProvisionedChanged() {
-                    trySend(deviceProvisionedController.isDeviceProvisioned)
+                    trySend(isDeviceProvisioned())
                 }
             }
         deviceProvisionedController.addCallback(listener)
-        trySend(deviceProvisionedController.isDeviceProvisioned)
+        trySend(isDeviceProvisioned())
         awaitClose { deviceProvisionedController.removeCallback(listener) }
     }
+
+    override fun isDeviceProvisioned(): Boolean {
+        return deviceProvisionedController.isDeviceProvisioned
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
index 66ed092..ace4ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
@@ -26,7 +26,7 @@
 class DeviceProvisioningInteractor
 @Inject
 constructor(
-    repository: DeviceProvisioningRepository,
+    private val repository: DeviceProvisioningRepository,
 ) {
     /**
      * Whether this device has been provisioned.
@@ -34,4 +34,8 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned
+
+    fun isDeviceProvisioned(): Boolean {
+        return repository.isDeviceProvisioned()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index e4d0668..7a521a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -16,8 +16,14 @@
 
 package com.android.systemui.statusbar.policy.domain.interactor
 
+import android.content.Context
 import android.provider.Settings
+import androidx.concurrent.futures.await
 import com.android.settingslib.notification.data.repository.ZenModeRepository
+import com.android.settingslib.notification.modes.ZenIconLoader
+import com.android.settingslib.notification.modes.ZenMode
+import com.android.systemui.common.shared.model.Icon
+import java.time.Duration
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -28,7 +34,9 @@
  * An interactor that performs business logic related to the status and configuration of Zen Mode
  * (or Do Not Disturb/DND Mode).
  */
-class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) {
+class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) {
+    private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance()
+
     val isZenModeEnabled: Flow<Boolean> =
         repository.globalZenMode
             .map {
@@ -52,4 +60,18 @@
                 }
             }
             .distinctUntilChanged()
+
+    val modes: Flow<List<ZenMode>> = repository.modes
+
+    suspend fun getModeIcon(mode: ZenMode, context: Context): Icon {
+        return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null)
+    }
+
+    fun activateMode(zenMode: ZenMode, duration: Duration? = null) {
+        repository.activateMode(zenMode, duration)
+    }
+
+    fun deactivateMode(zenMode: ZenMode) {
+        repository.deactivateMode(zenMode)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index 6db1eac..2b094d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import com.android.systemui.statusbar.phone.create
+import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
 import javax.inject.Inject
 
 class ModesDialogDelegate
@@ -37,12 +39,13 @@
     private val sysuiDialogFactory: SystemUIDialogFactory,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val activityStarter: ActivityStarter,
+    private val viewModel: ModesDialogViewModel,
 ) : SystemUIDialog.Delegate {
     override fun createDialog(): SystemUIDialog {
         return sysuiDialogFactory.create { dialog ->
             AlertDialogContent(
                 title = { Text(stringResource(R.string.zen_modes_dialog_title)) },
-                content = { Text("Under construction") },
+                content = { ModeTileGrid(viewModel) },
                 neutralButton = {
                     PlatformOutlinedButton(
                         onClick = {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
new file mode 100644
index 0000000..91bfdff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.statusbar.policy.ui.dialog.composable
+
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel
+
+@Composable
+fun ModeTile(viewModel: ModeTileViewModel) {
+    val tileColor =
+        if (viewModel.enabled) MaterialTheme.colorScheme.primary
+        else MaterialTheme.colorScheme.surfaceVariant
+    val contentColor =
+        if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary
+        else MaterialTheme.colorScheme.onSurfaceVariant
+
+    CompositionLocalProvider(LocalContentColor provides contentColor) {
+        Surface(
+            color = tileColor,
+            shape = RoundedCornerShape(16.dp),
+            modifier =
+                Modifier.combinedClickable(
+                    onClick = viewModel.onClick,
+                    onLongClick = viewModel.onLongClick
+                ),
+        ) {
+            Row(
+                modifier = Modifier.padding(20.dp),
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement =
+                    Arrangement.spacedBy(
+                        space = 10.dp,
+                        alignment = Alignment.Start,
+                    ),
+            ) {
+                Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp))
+                Column {
+                    Text(
+                        viewModel.text,
+                        fontWeight = FontWeight.W500,
+                        modifier = Modifier.tileMarquee()
+                    )
+                    Text(
+                        viewModel.subtext,
+                        fontWeight = FontWeight.W400,
+                        modifier = Modifier.tileMarquee()
+                    )
+                }
+            }
+        }
+    }
+}
+
+private fun Modifier.tileMarquee(): Modifier {
+    return this.basicMarquee(
+        iterations = 1,
+        initialDelayMillis = 200,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
new file mode 100644
index 0000000..73d361f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.statusbar.policy.ui.dialog.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
+
+@Composable
+fun ModeTileGrid(viewModel: ModesDialogViewModel) {
+    val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList())
+
+    // TODO(b/346519570): Handle what happens when we have more than a few modes.
+    LazyVerticalGrid(
+        columns = GridCells.Fixed(2),
+        modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp),
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
+        items(
+            tiles.size,
+            key = { index -> tiles[index].id },
+        ) { index ->
+            ModeTile(viewModel = tiles[index])
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
new file mode 100644
index 0000000..5bd26cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.statusbar.policy.ui.dialog.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+
+/**
+ * Viewmodel for a tile representing a single priority ("zen") mode, for use within the modes
+ * dialog. Not to be confused with ModesTile, which is the Quick Settings tile that opens the
+ * dialog.
+ */
+data class ModeTileViewModel(
+    val id: String,
+    val icon: Icon,
+    val text: String,
+    val subtext: String,
+    val enabled: Boolean,
+    val contentDescription: String,
+    val onClick: () -> Unit,
+    val onLongClick: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
new file mode 100644
index 0000000..e84c8b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.statusbar.policy.ui.dialog.viewmodel
+
+import android.content.Context
+import com.android.settingslib.notification.modes.ZenMode
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows
+ * the user to quickly toggle modes.
+ */
+@SysUISingleton
+class ModesDialogViewModel
+@Inject
+constructor(
+    val context: Context,
+    zenModeInteractor: ZenModeInteractor,
+    @Background val bgDispatcher: CoroutineDispatcher,
+) {
+    // Modes that should be displayed in the dialog
+    // TODO(b/346519570): Include modes that have not been set up yet.
+    private val visibleModes: Flow<List<ZenMode>> =
+        zenModeInteractor.modes.map {
+            it.filter { mode ->
+                mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed)
+            }
+        }
+
+    val tiles: Flow<List<ModeTileViewModel>> =
+        visibleModes
+            .map { modesList ->
+                modesList.map { mode ->
+                    ModeTileViewModel(
+                        id = mode.id,
+                        icon = zenModeInteractor.getModeIcon(mode, context),
+                        text = mode.rule.name,
+                        subtext = getTileSubtext(mode),
+                        enabled = mode.isActive,
+                        // TODO(b/346519570): This should be some combination of the above, e.g.
+                        //  "ON: Do Not Disturb, Until Mon 08:09"; see DndTile.
+                        contentDescription = "",
+                        onClick = {
+                            if (mode.isActive) {
+                                zenModeInteractor.deactivateMode(mode)
+                            } else {
+                                // TODO(b/346519570): Handle duration for DND mode.
+                                zenModeInteractor.activateMode(mode)
+                            }
+                        },
+                        onLongClick = {
+                            // TODO(b/346519570): Open settings page for mode.
+                        }
+                    )
+                }
+            }
+            .flowOn(bgDispatcher)
+
+    private fun getTileSubtext(mode: ZenMode): String {
+        // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND
+        val on = context.resources.getString(R.string.zen_mode_on)
+        val off = context.resources.getString(R.string.zen_mode_off)
+        return mode.rule.triggerDescription ?: if (mode.isActive) on else off
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index c45f98e..066bfc5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -18,6 +18,8 @@
 
 import static android.media.AudioManager.RINGER_MODE_NORMAL;
 
+import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix;
+
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.app.NotificationManager;
@@ -59,6 +61,8 @@
 import android.view.accessibility.CaptioningManager;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Observer;
 
 import com.android.internal.annotations.GuardedBy;
@@ -76,6 +80,8 @@
 import com.android.systemui.util.RingerModeLiveData;
 import com.android.systemui.util.RingerModeTracker;
 import com.android.systemui.util.concurrency.ThreadFactory;
+import com.android.systemui.util.kotlin.JavaAdapter;
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractor;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -102,7 +108,13 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000;
-    private static final int DYNAMIC_STREAM_START_INDEX = 100;
+    // We only need one dynamic stream for broadcast because at most two headsets are allowed
+    // to join local broadcast in current stage.
+    // It is safe to use 99 as the broadcast stream now. There are only 10+ default audio
+    // streams defined in AudioSystem for now and audio team is in the middle of restructure,
+    // no new default stream is preferred.
+    @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99;
+    private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
     private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES =
             new AudioAttributes.Builder()
                     .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -145,6 +157,8 @@
     private final State mState = new State();
     protected final MediaSessionsCallbacks mMediaSessionsCallbacksW;
     private final VibratorHelper mVibrator;
+    private final AudioSharingInteractor mAudioSharingInteractor;
+    private final JavaAdapter mJavaAdapter;
     private final boolean mHasVibrator;
     private boolean mShowA11yStream;
     private boolean mShowVolumeDialog;
@@ -188,7 +202,9 @@
             KeyguardManager keyguardManager,
             ActivityManager activityManager,
             UserTracker userTracker,
-            DumpManager dumpManager
+            DumpManager dumpManager,
+            AudioSharingInteractor audioSharingInteractor,
+            JavaAdapter javaAdapter
     ) {
         mContext = context.getApplicationContext();
         mPackageManager = packageManager;
@@ -200,6 +216,8 @@
         mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
         mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext);
         mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW);
+        mAudioSharingInteractor = audioSharingInteractor;
+        mJavaAdapter = javaAdapter;
         mAudio = audioManager;
         mNoMan = notificationManager;
         mObserver = new SettingObserver(mWorker);
@@ -272,6 +290,12 @@
         } catch (SecurityException e) {
             Log.w(TAG, "No access to media sessions", e);
         }
+        if (volumeDialogAudioSharingFix()) {
+            Slog.d(TAG, "Start collect volume changes in audio sharing");
+            mJavaAdapter.alwaysCollectFlow(
+                    mAudioSharingInteractor.getVolume(),
+                    this::handleAudioSharingStreamVolumeChanges);
+        }
     }
 
     public void setVolumePolicy(VolumePolicy policy) {
@@ -545,7 +569,13 @@
         mState.activeStream = activeStream;
         Events.writeEvent(Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream);
         if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream);
-        final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1;
+        final int s =
+                activeStream
+                                < (volumeDialogAudioSharingFix()
+                                        ? DYNAMIC_STREAM_BROADCAST
+                                        : DYNAMIC_STREAM_REMOTE_START_INDEX)
+                        ? activeStream
+                        : -1;
         if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s);
         mAudio.forceVolumeControlStream(s);
         return true;
@@ -726,7 +756,12 @@
 
     private void onSetStreamVolumeW(int stream, int level) {
         if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level);
-        if (stream >= DYNAMIC_STREAM_START_INDEX) {
+        if (volumeDialogAudioSharingFix() && stream == DYNAMIC_STREAM_BROADCAST) {
+            Slog.d(TAG, "onSetStreamVolumeW set broadcast stream level = " + level);
+            mAudioSharingInteractor.setStreamVolume(level);
+            return;
+        }
+        if (stream >= DYNAMIC_STREAM_REMOTE_START_INDEX) {
             mMediaSessionsCallbacksW.setStreamVolume(stream, level);
             return;
         }
@@ -758,6 +793,40 @@
         DndTile.setVisible(mContext, true);
     }
 
+    void handleAudioSharingStreamVolumeChanges(@Nullable Integer volume) {
+        if (volume == null) {
+            if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) {
+                mState.states.remove(DYNAMIC_STREAM_BROADCAST);
+                Slog.d(TAG, "Remove audio sharing stream");
+                mCallbacks.onStateChanged(mState);
+            }
+        } else {
+            if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) {
+                StreamState ss = mState.states.get(DYNAMIC_STREAM_BROADCAST);
+                if (ss.level != volume) {
+                    ss.level = volume;
+                    Slog.d(TAG, "updateState, audio sharing stream volume = " + volume);
+                    mCallbacks.onStateChanged(mState);
+                }
+            } else {
+                StreamState ss = streamStateW(DYNAMIC_STREAM_BROADCAST);
+                ss.dynamic = true;
+                ss.levelMin = mAudioSharingInteractor.getVolumeMin();
+                ss.levelMax = mAudioSharingInteractor.getVolumeMax();
+                if (ss.level != volume) {
+                    ss.level = volume;
+                }
+                String label = mContext.getString(R.string.audio_sharing_description);
+                if (!Objects.equals(ss.remoteLabel, label)) {
+                    ss.name = -1;
+                    ss.remoteLabel = label;
+                }
+                Slog.d(TAG, "updateState, new audio sharing stream volume = " + volume);
+                mCallbacks.onStateChanged(mState);
+            }
+        }
+    }
+
     private final class VC extends IVolumeController.Stub {
         private final String TAG = VolumeDialogControllerImpl.TAG + ".VC";
 
@@ -1256,7 +1325,7 @@
     protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks {
         private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
 
-        private int mNextStream = DYNAMIC_STREAM_START_INDEX;
+        private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX;
         private final boolean mVolumeAdjustmentForRemoteGroupSessions;
 
         public MediaSessionsCallbacks(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 6b02e1a..0770d89 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -34,6 +34,7 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
+import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix;
 import static com.android.systemui.Flags.hapticVolumeSlider;
 import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
@@ -1678,6 +1679,14 @@
                 return true;
             }
 
+            // Always show the stream for audio sharing if it exists.
+            if (volumeDialogAudioSharingFix()
+                    && row.ss != null
+                    && mContext.getString(R.string.audio_sharing_description)
+                            .equals(row.ss.remoteLabel)) {
+                return true;
+            }
+
             if (row.defaultStream) {
                 return activeRow.stream == STREAM_RING
                         || activeRow.stream == STREAM_ALARM
@@ -1880,10 +1889,25 @@
             if (!ss.dynamic) continue;
             mDynamic.put(stream, true);
             if (findRow(stream) == null) {
-                addRow(stream,
-                        com.android.settingslib.R.drawable.ic_volume_remote,
-                        com.android.settingslib.R.drawable.ic_volume_remote_mute,
-                        true, false, true);
+                if (volumeDialogAudioSharingFix()
+                        && mContext.getString(R.string.audio_sharing_description)
+                                .equals(ss.remoteLabel)) {
+                    addRow(
+                            stream,
+                            R.drawable.ic_volume_media,
+                            R.drawable.ic_volume_media_mute,
+                            true,
+                            false,
+                            true);
+                } else {
+                    addRow(
+                            stream,
+                            com.android.settingslib.R.drawable.ic_volume_remote,
+                            com.android.settingslib.R.drawable.ic_volume_remote_mute,
+                            true,
+                            false,
+                            true);
+                }
             }
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index d940fc9..7a4bbfe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -36,7 +36,6 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -77,9 +76,6 @@
 
         when(mBatteryMeterView.getContext()).thenReturn(mContext);
         when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
-
-        mContext.getOrCreateTestableResources().addOverride(
-                R.bool.flag_battery_shield_icon, false);
     }
 
     @Test
@@ -136,26 +132,6 @@
         verify(mTunerService, never()).addTunable(any(), any());
     }
 
-    @Test
-    public void shieldFlagDisabled_viewNotified() {
-        mContext.getOrCreateTestableResources().addOverride(
-                R.bool.flag_battery_shield_icon, false);
-
-        initController();
-
-        verify(mBatteryMeterView).setDisplayShieldEnabled(false);
-    }
-
-    @Test
-    public void shieldFlagEnabled_viewNotified() {
-        mContext.getOrCreateTestableResources().addOverride(
-                R.bool.flag_battery_shield_icon, true);
-
-        initController();
-
-        verify(mBatteryMeterView).setDisplayShieldEnabled(true);
-    }
-
     private void initController() {
         mController = new BatteryMeterViewController(
                 mBatteryMeterView,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index 2bd0976..2aa33a17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -22,9 +22,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.flags.Flags.FLAG_NEW_STATUS_BAR_ICONS
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -67,9 +67,8 @@
     fun contentDescription_unknown() {
         mBatteryMeterView.onBatteryUnknownStateChanged(true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_unknown)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_unknown))
     }
 
     @Test
@@ -80,11 +79,10 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(
-                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
-                )
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
+                context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
+            )
     }
 
     @Test
@@ -96,13 +94,14 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
                 context.getString(
-                        R.string.accessibility_battery_level_charging_paused_with_estimate,
-                        17,
-                        ESTIMATE,
+                    R.string.accessibility_battery_level_charging_paused_with_estimate,
+                    17,
+                    ESTIMATE,
                 )
-        )
+            )
     }
 
     @Test
@@ -110,27 +109,24 @@
         mBatteryMeterView.onBatteryLevelChanged(90, false)
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90))
     }
 
     @Test
     fun contentDescription_charging() {
         mBatteryMeterView.onBatteryLevelChanged(45, true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging, 45)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 45))
     }
 
     @Test
     fun contentDescription_notCharging() {
         mBatteryMeterView.onBatteryLevelChanged(45, false)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 45)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 45))
     }
 
     @Test
@@ -138,9 +134,8 @@
         mBatteryMeterView.onBatteryLevelChanged(45, true)
         mBatteryMeterView.onIsIncompatibleChargingChanged(true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 45)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 45))
     }
 
     @Test
@@ -152,19 +147,17 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(
-                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
-                )
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
+                context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
+            )
 
         // Update the show mode from estimate to percent
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
 
         assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%")
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 15)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 15))
     }
 
     @Test
@@ -176,19 +169,17 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-            context.getString(
-                R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
+                context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
             )
-        )
 
         // Update the show mode from estimate to percent
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
 
         assertThat(mBatteryMeterView.batteryPercentView).isNull()
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-            context.getString(R.string.accessibility_battery_level, 15)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 15))
         assertThat(mBatteryMeterView.unifiedBatteryState.showPercent).isTrue()
     }
 
@@ -225,49 +216,47 @@
         // BatteryDefender
         mBatteryMeterView.onBatteryLevelChanged(90, false)
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90))
 
         // BatteryDefender & estimate
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
         mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
         mBatteryMeterView.updatePercentText()
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
                 context.getString(
-                        R.string.accessibility_battery_level_charging_paused_with_estimate,
-                        90,
-                        ESTIMATE,
+                    R.string.accessibility_battery_level_charging_paused_with_estimate,
+                    90,
+                    ESTIMATE,
                 )
-        )
+            )
 
         // Just estimate
         mBatteryMeterView.onIsBatteryDefenderChanged(false)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
                 context.getString(
-                        R.string.accessibility_battery_level_with_estimate,
-                        90,
-                        ESTIMATE,
+                    R.string.accessibility_battery_level_with_estimate,
+                    90,
+                    ESTIMATE,
                 )
-        )
+            )
 
         // Just percent
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 90))
 
         // Charging
         mBatteryMeterView.onBatteryLevelChanged(90, true)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 90))
     }
 
     @Test
     @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOff() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
         val drawable = getBatteryDrawable()
 
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
@@ -278,8 +267,6 @@
     @Test
     @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOn() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
-
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
 
         assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNotNull()
@@ -288,7 +275,6 @@
     @Test
     @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOff() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
         val drawable = getBatteryDrawable()
 
         // Start as true
@@ -303,8 +289,6 @@
     @Test
     @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOn() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
-
         // Start as true
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
 
@@ -316,27 +300,6 @@
 
     @Test
     @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
-    fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOff() {
-        mBatteryMeterView.setDisplayShieldEnabled(false)
-        val drawable = getBatteryDrawable()
-
-        mBatteryMeterView.onIsBatteryDefenderChanged(true)
-
-        assertThat(drawable.displayShield).isFalse()
-    }
-
-    @Test
-    @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
-    fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOn() {
-        mBatteryMeterView.setDisplayShieldEnabled(false)
-
-        mBatteryMeterView.onIsBatteryDefenderChanged(true)
-
-        assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNull()
-    }
-
-    @Test
-    @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isIncompatibleChargingChanged_true_drawableGetsChargingFalse_flagOff() {
         mBatteryMeterView.onBatteryLevelChanged(45, true)
         val drawable = getBatteryDrawable()
@@ -381,8 +344,8 @@
     }
 
     private fun getBatteryDrawable(): AccessorizedBatteryDrawable {
-        return (mBatteryMeterView.getChildAt(0) as ImageView)
-                .drawable as AccessorizedBatteryDrawable
+        return (mBatteryMeterView.getChildAt(0) as ImageView).drawable
+            as AccessorizedBatteryDrawable
     }
 
     private class Fetcher : BatteryEstimateFetcher {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 8a6b68f..a5c4bcd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -675,8 +675,14 @@
 
         mMainHandler = new Handler(Looper.getMainLooper());
 
+        LongPressHandlingView longPressHandlingView = mock(LongPressHandlingView.class);
         when(mView.requireViewById(R.id.keyguard_long_press))
-                .thenReturn(mock(LongPressHandlingView.class));
+                .thenReturn(longPressHandlingView);
+
+        Resources longPressHandlingViewRes = mock(Resources.class);
+        when(longPressHandlingView.getResources()).thenReturn(longPressHandlingViewRes);
+        when(longPressHandlingViewRes.getString(anyInt())).thenReturn("");
+
 
         mHeadsUpNotificationInteractor =
                 new HeadsUpNotificationInteractor(mFakeHeadsUpNotificationRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 52af907..64eadb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index acb005f..0407fc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -42,7 +42,7 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 // this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.V2.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
 class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
     lateinit var manager: NotificationSectionsFeatureManager
     private val proxyFake = DeviceConfigProxyFake()
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 a925ccf..c971037 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
@@ -1197,6 +1197,26 @@
 
     @Test
     @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+    public void testGenerateHeadsUpAnimation_isSeenInShade_noAnimation() {
+        // GIVEN NSSL is ready for HUN animations
+        Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class);
+        prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener);
+
+        // Entry was seen in shade
+        NotificationEntry entry = mock(NotificationEntry.class);
+        when(entry.isSeenInShade()).thenReturn(true);
+        ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
+        when(row.getEntry()).thenReturn(entry);
+
+        // WHEN we generate an add event
+        mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true);
+
+        // THEN nothing happens
+        assertThat(mStackScroller.isAddOrRemoveAnimationPending()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
     public void testOnChildAnimationsFinished_resetsheadsUpAnimatingAway() {
         // GIVEN NSSL is ready for HUN animations
         Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 49e3f04..31f93b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1059,6 +1059,7 @@
 
     @Test
     @DisableSceneContainer
+    @DisableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index cc2ef53..12cfdcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.batteryController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
index f40282f..a8271fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
@@ -21,10 +21,9 @@
 import android.view.MotionEvent
 import android.view.MotionEvent.CLASSIFICATION_NONE
 import android.view.MotionEvent.TOOL_TYPE_FINGER
-import java.lang.reflect.Method
-import org.mockito.kotlin.doNothing
+import org.mockito.AdditionalAnswers
+import org.mockito.Mockito.mock
 import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 
 fun motionEvent(
@@ -40,31 +39,18 @@
     val event =
         MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0)
     event.source = source
-    val spy =
-        spy<MotionEvent>(event) {
-            on { getToolType(0) } doReturn toolType
-            on { getPointerCount() } doReturn pointerCount
-            axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value }
-            on { getClassification() } doReturn classification
-        }
-    ensureFinalizeIsNotCalledTwice(spy)
-    return spy
-}
-
-private fun ensureFinalizeIsNotCalledTwice(spy: MotionEvent) {
-    // Spy in mockito will create copy of the spied object, copying all its field etc. Here it means
-    // we create copy of MotionEvent and its mNativePtr, so we have two separate objects of type
-    // MotionEvents with the same mNativePtr. That breaks because MotionEvent has custom finalize()
-    // method which goes to native code and tries to delete the reference from mNativePtr. It works
-    // first time but second time reference is already deleted and it breaks. That's why we have to
-    // avoid calling finalize twice
-    doNothing().whenever(spy).finalizeUsingReflection()
-}
-
-private fun MotionEvent.finalizeUsingReflection() {
-    val finalizeMethod: Method = MotionEvent::class.java.getDeclaredMethod("finalize")
-    finalizeMethod.isAccessible = true
-    finalizeMethod.invoke(this)
+    // we need to use mock with delegation instead of spy because:
+    // 1. Spy will try to deallocate the same memory again when finalize() is called as it keep the
+    // same memory pointer to native MotionEvent
+    // 2. Even after workaround for issue above there still remains problem with destructor of
+    // native event trying to free the same chunk of native memory. I'm not sure why it happens but
+    // mock seems to fix the issue and because it delegates all calls seems safer overall
+    val delegate = mock(MotionEvent::class.java, AdditionalAnswers.delegatesTo<MotionEvent>(event))
+    doReturn(toolType).whenever(delegate).getToolType(0)
+    doReturn(pointerCount).whenever(delegate).pointerCount
+    doReturn(classification).whenever(delegate).classification
+    axisValues.forEach { (key, value) -> doReturn(value).whenever(delegate).getAxisValue(key) }
+    return delegate
 }
 
 fun touchpadEvent(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index f737148..3f5dc82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -18,6 +18,8 @@
 
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -37,16 +39,19 @@
 import android.media.session.MediaSession;
 import android.os.Handler;
 import android.os.Process;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.settingslib.flags.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.RingerModeLiveData;
@@ -54,7 +59,9 @@
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.concurrency.FakeThreadFactory;
 import com.android.systemui.util.concurrency.ThreadFactory;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -63,6 +70,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 @RunWith(AndroidJUnit4.class)
@@ -104,6 +112,10 @@
     private UserTracker mUserTracker;
     @Mock
     private DumpManager mDumpManager;
+    @Mock
+    private AudioSharingInteractor mAudioSharingInteractor;
+    @Mock
+    private JavaAdapter mJavaAdapter;
 
 
     @Before
@@ -124,11 +136,26 @@
 
         mCallback = mock(VolumeDialogControllerImpl.C.class);
         mThreadFactory.setLooper(TestableLooper.get(this).getLooper());
-        mVolumeController = new TestableVolumeDialogControllerImpl(mContext,
-                mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager,
-                mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager,
-                mPackageManager, mWakefullnessLifcycle, mKeyguardManager,
-                mActivityManager, mUserTracker, mDumpManager, mCallback);
+        mVolumeController =
+                new TestableVolumeDialogControllerImpl(
+                        mContext,
+                        mBroadcastDispatcher,
+                        mRingerModeTracker,
+                        mThreadFactory,
+                        mAudioManager,
+                        mNotificationManager,
+                        mVibrator,
+                        mIAudioService,
+                        mAccessibilityManager,
+                        mPackageManager,
+                        mWakefullnessLifcycle,
+                        mKeyguardManager,
+                        mActivityManager,
+                        mUserTracker,
+                        mDumpManager,
+                        mCallback,
+                        mAudioSharingInteractor,
+                        mJavaAdapter);
         mVolumeController.setEnableDialogs(true, true);
     }
 
@@ -224,6 +251,41 @@
         verify(mUserTracker).addCallback(any(UserTracker.Callback.class), any(Executor.class));
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    public void handleAudioSharingStreamVolumeChanges_updateState() {
+        ArgumentCaptor<VolumeDialogController.State> stateCaptor =
+                ArgumentCaptor.forClass(VolumeDialogController.State.class);
+        int broadcastStream = VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST;
+
+        mVolumeController.handleAudioSharingStreamVolumeChanges(100);
+
+        verify(mCallback).onStateChanged(stateCaptor.capture());
+        assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue();
+        assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(100);
+
+        mVolumeController.handleAudioSharingStreamVolumeChanges(200);
+
+        verify(mCallback, times(2)).onStateChanged(stateCaptor.capture());
+        assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue();
+        assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(200);
+
+        mVolumeController.handleAudioSharingStreamVolumeChanges(null);
+
+        verify(mCallback, times(3)).onStateChanged(stateCaptor.capture());
+        assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    public void testSetStreamVolume_setSecondaryDeviceVolume() {
+        mVolumeController.setStreamVolume(
+                VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST, /* level= */ 100);
+        Objects.requireNonNull(TestableLooper.get(this)).processAllMessages();
+
+        verify(mAudioSharingInteractor).setStreamVolume(100);
+    }
+
     static class TestableVolumeDialogControllerImpl extends VolumeDialogControllerImpl {
         private final WakefulnessLifecycle.Observer mWakefullessLifecycleObserver;
 
@@ -243,11 +305,27 @@
                 ActivityManager activityManager,
                 UserTracker userTracker,
                 DumpManager dumpManager,
-                C callback) {
-            super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager,
-                    notificationManager, optionalVibrator, iAudioService, accessibilityManager,
-                    packageManager, wakefulnessLifecycle, keyguardManager,
-                    activityManager, userTracker, dumpManager);
+                C callback,
+                AudioSharingInteractor audioSharingInteractor,
+                JavaAdapter javaAdapter) {
+            super(
+                    context,
+                    broadcastDispatcher,
+                    ringerModeTracker,
+                    theadFactory,
+                    audioManager,
+                    notificationManager,
+                    optionalVibrator,
+                    iAudioService,
+                    accessibilityManager,
+                    packageManager,
+                    wakefulnessLifecycle,
+                    keyguardManager,
+                    activityManager,
+                    userTracker,
+                    dumpManager,
+                    audioSharingInteractor,
+                    javaAdapter);
             mCallbacks = callback;
 
             ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor =
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 cdfcca6..b5cbf59 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -23,6 +23,7 @@
 import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER;
 import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
 import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
+import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST;
 import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
 
 import static junit.framework.Assert.assertEquals;
@@ -72,6 +73,7 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.testing.UiEventLoggerFake;
+import com.android.settingslib.flags.Flags;
 import com.android.systemui.Prefs;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
@@ -794,6 +796,38 @@
         verify(mVolumeDialogInteractor, atLeastOnce()).onDialogDismissed(); // dismiss by timeout
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    public void testDynamicStreamForBroadcast_createRow() {
+        State state = createShellState();
+        VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState();
+        ss.dynamic = true;
+        ss.levelMin = 0;
+        ss.levelMax = 255;
+        ss.level = 20;
+        ss.name = -1;
+        ss.remoteLabel = mContext.getString(R.string.audio_sharing_description);
+        state.states.append(DYNAMIC_STREAM_BROADCAST, ss);
+
+        mDialog.onStateChangedH(state);
+        mTestableLooper.processAllMessages();
+
+        ViewGroup volumeDialogRows = mDialog.getDialogView().findViewById(R.id.volume_dialog_rows);
+        assumeNotNull(volumeDialogRows);
+        View broadcastRow = null;
+        final int rowCount = volumeDialogRows.getChildCount();
+        // we don't make assumptions about the position of the dnd row
+        for (int i = 0; i < rowCount; i++) {
+            View volumeRow = volumeDialogRows.getChildAt(i);
+            if (volumeRow.getId() == DYNAMIC_STREAM_BROADCAST) {
+                broadcastRow = volumeRow;
+                break;
+            }
+        }
+        assertNotNull(broadcastRow);
+        assertEquals(broadcastRow.getVisibility(), View.VISIBLE);
+    }
+
     /**
      * @return true if at least one volume row has the DND icon
      */
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt
index 1da1fb2..5e870b1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt
@@ -25,20 +25,11 @@
 /** Fake implementation of [CommunalPrefsRepository] */
 class FakeCommunalPrefsRepository : CommunalPrefsRepository {
     private val _isCtaDismissed = MutableStateFlow<Set<UserInfo>>(emptySet())
-    private val _isDisclaimerDismissed = MutableStateFlow<Set<UserInfo>>(emptySet())
 
     override fun isCtaDismissed(user: UserInfo): Flow<Boolean> =
         _isCtaDismissed.map { it.contains(user) }
 
-    override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> =
-        _isDisclaimerDismissed.map { it.contains(user) }
-
     override suspend fun setCtaDismissed(user: UserInfo) {
         _isCtaDismissed.value = _isCtaDismissed.value.toMutableSet().apply { add(user) }
     }
-
-    override suspend fun setDisclaimerDismissed(user: UserInfo) {
-        _isDisclaimerDismissed.value =
-            _isDisclaimerDismissed.value.toMutableSet().apply { add(user) }
-    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index eb92785..4ad046c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -42,6 +43,7 @@
     CommunalInteractor(
         applicationScope = applicationCoroutineScope,
         bgDispatcher = testDispatcher,
+        bgScope = testScope.backgroundScope,
         broadcastDispatcher = broadcastDispatcher,
         communalSceneInteractor = communalSceneInteractor,
         widgetRepository = communalWidgetRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
index f73f43d..edf4bcc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
@@ -17,7 +17,10 @@
 package com.android.systemui.education.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import java.time.Clock
 import java.time.Instant
 
 var Kosmos.contextualEducationRepository: ContextualEducationRepository by
-    Kosmos.Fixture { FakeContextualEducationRepository(FakeEduClock(Instant.MIN)) }
+    Kosmos.Fixture { FakeContextualEducationRepository(fakeEduClock) }
+
+var Kosmos.fakeEduClock: Clock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
new file mode 100644
index 0000000..5b2dc2b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.systemui.education.domain.interactor
+
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+
+val Kosmos.contextualEducationInteractor by
+    Kosmos.Fixture {
+        ContextualEducationInteractor(
+            backgroundScope = testScope.backgroundScope,
+            repository = contextualEducationRepository,
+            selectedUserInteractor = selectedUserInteractor
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
new file mode 100644
index 0000000..8f84e04
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.systemui.education.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+
+var Kosmos.keyboardTouchpadEduStatsInteractor by
+    Kosmos.Fixture {
+        KeyboardTouchpadEduStatsInteractorImpl(
+            backgroundScope = testScope.backgroundScope,
+            contextualEducationInteractor = contextualEducationInteractor
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
index da956ec..8b4de2b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
@@ -22,13 +22,12 @@
  * The [modifier] function will be passed an instance of a NotificationEntryBuilder. Any
  * modifications made to the builder will be applied to the [entry].
  */
-inline fun modifyEntry(
-    entry: NotificationEntry,
+inline fun NotificationEntry.modifyEntry(
     crossinline modifier: NotificationEntryBuilder.() -> Unit
 ) {
-    val builder = NotificationEntryBuilder(entry)
+    val builder = NotificationEntryBuilder(this)
     modifier(builder)
-    builder.apply(entry)
+    builder.apply(this)
 }
 
 fun getAttachState(entry: ListEntry): ListAttachState {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
new file mode 100644
index 0000000..77d97bb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.util.settings.fakeSettings
+
+var Kosmos.lockScreenMinimalismCoordinator by
+    Kosmos.Fixture {
+        LockScreenMinimalismCoordinator(
+            bgDispatcher = testDispatcher,
+            dumpManager = dumpManager,
+            headsUpInteractor = headsUpNotificationInteractor,
+            logger = lockScreenMinimalismCoordinatorLogger,
+            scope = testScope.backgroundScope,
+            secureSettings = fakeSettings,
+            seenNotificationsInteractor = seenNotificationsInteractor,
+            statusBarStateController = statusBarStateController,
+            shadeInteractor = shadeInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
new file mode 100644
index 0000000..77aeb44
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.lockScreenMinimalismCoordinatorLogger by
+    Kosmos.Fixture { LockScreenMinimalismCoordinatorLogger(logcatLogBuffer()) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 492e87b..7e8f1a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -22,14 +22,19 @@
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
 
 val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
 
 class FakeHeadsUpNotificationRepository : HeadsUpRepository {
     override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
-    override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
-    override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
-        MutableStateFlow(emptySet())
+
+    val orderedHeadsUpRows = MutableStateFlow(emptyList<HeadsUpRowRepository>())
+    override val topHeadsUpRow: Flow<HeadsUpRowRepository?> =
+        orderedHeadsUpRows.map { it.firstOrNull() }.distinctUntilChanged()
+    override val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> =
+        orderedHeadsUpRows.map { it.toSet() }.distinctUntilChanged()
 
     override fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
         isHeadsUpAnimatingAway.value = animatingAway
@@ -38,4 +43,12 @@
     override fun snooze() {
         // do nothing
     }
+
+    fun setNotifications(notifications: List<HeadsUpRowRepository>) {
+        this.orderedHeadsUpRows.value = notifications.toList()
+    }
+
+    fun setNotifications(vararg notifications: HeadsUpRowRepository) {
+        this.orderedHeadsUpRows.value = notifications.toList()
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
deleted file mode 100644
index 9be7dfe..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
+++ /dev/null
@@ -1,27 +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.systemui.statusbar.notification.stack.data.repository
-
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
-
-fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
-    setNotifications(*notifications.toTypedArray())
-}
-
-fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
-    this.activeHeadsUpRows.value = notifications.toSet()
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
index 9247e88..e3176f1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
@@ -26,9 +26,14 @@
 class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository {
     private val _isDeviceProvisioned = MutableStateFlow(true)
     override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned
+
     fun setDeviceProvisioned(isProvisioned: Boolean) {
         _isDeviceProvisioned.value = isProvisioned
     }
+
+    override fun isDeviceProvisioned(): Boolean {
+        return _isDeviceProvisioned.value
+    }
 }
 
 @Module
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
new file mode 100644
index 0000000..db95fad
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_Fail01_Test {
+    private ExpectedException mThrown = ExpectedException.none();
+    private final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Rule
+    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
+
+    public RavenwoodTestMethodValidation_Fail01_Test() {
+        mThrown.expectMessage("Method setUp() doesn't have @Before");
+    }
+
+    @SuppressWarnings("JUnit4SetUpNotRun")
+    public void setUp() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
new file mode 100644
index 0000000..ddc66c7
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_Fail02_Test {
+    private ExpectedException mThrown = ExpectedException.none();
+    private final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Rule
+    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
+
+    public RavenwoodTestMethodValidation_Fail02_Test() {
+        mThrown.expectMessage("Method tearDown() doesn't have @After");
+    }
+
+    @SuppressWarnings("JUnit4TearDownNotRun")
+    public void tearDown() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
new file mode 100644
index 0000000..ec8e907
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_Fail03_Test {
+    private ExpectedException mThrown = ExpectedException.none();
+    private final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Rule
+    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
+
+    public RavenwoodTestMethodValidation_Fail03_Test() {
+        mThrown.expectMessage("Method testFoo() doesn't have @Test");
+    }
+
+    @SuppressWarnings("JUnit4TestNotRun")
+    public void testFoo() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
new file mode 100644
index 0000000..d952d07
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_OkTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Before
+    public void setUp() {
+    }
+
+    @Before
+    public void testSetUp() {
+    }
+
+    @After
+    public void tearDown() {
+    }
+
+    @After
+    public void testTearDown() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 49e793f..4357f2b 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -33,14 +33,17 @@
 import com.android.server.LocalServices;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.Description;
 import org.junit.runner.RunWith;
 import org.junit.runners.model.Statement;
 
 import java.io.PrintStream;
+import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
@@ -230,6 +233,18 @@
         }
     }
 
+    /**
+     * @return if a method has any of annotations.
+     */
+    private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) {
+        for (var anno : annotations) {
+            if (m.getAnnotation(anno) != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static void validateTestAnnotations(Statement base, Description description,
             boolean enableOptionalValidation) {
         final var testClass = description.getTestClass();
@@ -239,13 +254,14 @@
         boolean hasErrors = false;
         for (Method m : collectMethods(testClass)) {
             if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) {
-                if (m.getAnnotation(Test.class) == null) {
+                if (!hasAnyAnnotations(m, Test.class, Before.class, After.class,
+                        BeforeClass.class, AfterClass.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @Test");
                     hasErrors = true;
                 }
             }
             if ("setUp".equals(m.getName())) {
-                if (m.getAnnotation(Before.class) == null) {
+                if (!hasAnyAnnotations(m, Before.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @Before");
                     hasErrors = true;
                 }
@@ -255,7 +271,7 @@
                 }
             }
             if ("tearDown".equals(m.getName())) {
-                if (m.getAnnotation(After.class) == null) {
+                if (!hasAnyAnnotations(m, After.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @After");
                     hasErrors = true;
                 }
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index b4efae3..8e2e0ad 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -114,6 +114,13 @@
 }
 
 flag {
+    name: "enable_magnification_follows_mouse"
+    namespace: "accessibility"
+    description: "Whether to enable mouse following for fullscreen magnification"
+    bug: "335494097"
+}
+
+flag {
     name: "fix_drag_pointer_when_ending_drag"
     namespace: "accessibility"
     description: "Send the correct pointer id when transitioning from dragging to delegating states."
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index d7da2f0..a5ec2ba 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -804,7 +804,15 @@
                     + event.mSuggestionPresentedLastTimestampMs
                     + " event.mFocusedVirtualAutofillId=" + event.mFocusedVirtualAutofillId
                     + " event.mFieldFirstLength=" + event.mFieldFirstLength
-                    + " event.mFieldLastLength=" + event.mFieldLastLength);
+                    + " event.mFieldLastLength=" + event.mFieldLastLength
+                    + " event.mViewFailedPriorToRefillCount=" + event.mViewFailedPriorToRefillCount
+                    + " event.mViewFilledSuccessfullyOnRefillCount="
+                    + event.mViewFilledSuccessfullyOnRefillCount
+                    + " event.mViewFailedOnRefillCount=" + event.mViewFailedOnRefillCount
+                    + " event.notExpiringResponseDuringAuthCount="
+                    + event.mFixExpireResponseDuringAuthCount
+                    + " event.notifyViewEnteredIgnoredDuringAuthCount="
+                    + event.mNotifyViewEnteredIgnoredDuringAuthCount);
         }
 
         // TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -859,7 +867,12 @@
                 event.mSuggestionPresentedLastTimestampMs,
                 event.mFocusedVirtualAutofillId,
                 event.mFieldFirstLength,
-                event.mFieldLastLength);
+                event.mFieldLastLength,
+                event.mViewFailedPriorToRefillCount,
+                event.mViewFilledSuccessfullyOnRefillCount,
+                event.mViewFailedOnRefillCount,
+                event.mFixExpireResponseDuringAuthCount,
+                event.mNotifyViewEnteredIgnoredDuringAuthCount);
         mEventInternal = Optional.empty();
     }
 
@@ -912,6 +925,12 @@
         // uninitialized doesn't help much, as this would be non-zero only if callback is received.
         int mViewFillSuccessCount = 0;
         int mViewFilledButUnexpectedCount = 0;
+        int mViewFailedPriorToRefillCount = 0;
+        int mViewFailedOnRefillCount = 0;
+        int mViewFilledSuccessfullyOnRefillCount = 0;
+
+        int mFixExpireResponseDuringAuthCount = 0;
+        int mNotifyViewEnteredIgnoredDuringAuthCount = 0;
 
         ArraySet<AutofillId> mAutofillIdsAttemptedAutofill;
         ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>();
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 7d9d660..ee7d0ae 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -28,9 +28,9 @@
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
-import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
 import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
 import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix;
+import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
 
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
@@ -561,8 +561,8 @@
     private void sendPendingIntent(int displayId, PendingIntent pendingIntent)
             throws PendingIntent.CanceledException {
         final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId);
-        options.setPendingIntentBackgroundActivityLaunchAllowed(true);
-        options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+        options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
         pendingIntent.send(
                 mContext,
                 /* code= */ 0,
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 092ee16..67985ef 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -102,7 +102,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.Clock;
@@ -1192,7 +1191,7 @@
                                     .setMinConsumedPowerThreshold(minConsumedPowerThreshold)
                                     .build();
                     bus = getBatteryUsageStats(List.of(query)).get(0);
-                    return new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
+                    return StatsPerUidLogger.logStats(bus, data);
                 }
                 default:
                     throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
@@ -1205,35 +1204,7 @@
         }
     }
 
-    public static class FrameworkStatsLogger {
-        /**
-         * Wrapper for the FrameworkStatsLog.buildStatsEvent method that makes it easier
-         * for mocking.
-         */
-        @VisibleForTesting
-        public StatsEvent buildStatsEvent(long sessionStartTs, long sessionEndTs,
-                long sessionDuration, int sessionDischargePercentage, long sessionDischargeDuration,
-                int uid, @BatteryConsumer.ProcessState int processState, long timeInStateMillis,
-                String powerComponentName, float totalConsumedPowerMah, float powerComponentMah,
-                long powerComponentDurationMillis) {
-            return FrameworkStatsLog.buildStatsEvent(
-                    FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID,
-                    sessionStartTs,
-                    sessionEndTs,
-                    sessionDuration,
-                    sessionDischargePercentage,
-                    sessionDischargeDuration,
-                    uid,
-                    processState,
-                    timeInStateMillis,
-                    powerComponentName,
-                    totalConsumedPowerMah,
-                    powerComponentMah,
-                    powerComponentDurationMillis);
-        }
-    }
-
-    public static class StatsPerUidLogger {
+    private static class StatsPerUidLogger {
 
         private static final int STATSD_METRIC_MAX_DIMENSIONS_COUNT = 3000;
 
@@ -1253,18 +1224,7 @@
                 long dischargeDuration) {}
         ;
 
-        private final FrameworkStatsLogger mFrameworkStatsLogger;
-
-        public StatsPerUidLogger(FrameworkStatsLogger frameworkStatsLogger) {
-            mFrameworkStatsLogger = frameworkStatsLogger;
-        }
-
-        /**
-         * Generates StatsEvents for the supplied battery usage stats and adds them to
-         * the supplied list.
-         */
-        @VisibleForTesting
-        public int logStats(BatteryUsageStats bus, List<StatsEvent> data) {
+        static int logStats(BatteryUsageStats bus, List<StatsEvent> data) {
             final SessionInfo sessionInfo =
                     new SessionInfo(
                             bus.getStatsStartTimestamp(),
@@ -1380,7 +1340,7 @@
             return StatsManager.PULL_SUCCESS;
         }
 
-        private boolean addStatsForPredefinedComponent(
+        private static boolean addStatsForPredefinedComponent(
                 List<StatsEvent> data,
                 SessionInfo sessionInfo,
                 int uid,
@@ -1420,7 +1380,7 @@
                     powerComponentDurationMillis);
         }
 
-        private boolean addStatsForCustomComponent(
+        private static boolean addStatsForCustomComponent(
                 List<StatsEvent> data,
                 SessionInfo sessionInfo,
                 int uid,
@@ -1462,7 +1422,7 @@
          * Returns true on success and false if reached max atoms capacity and no more atoms should
          * be added
          */
-        private boolean addStatsAtom(
+        private static boolean addStatsAtom(
                 List<StatsEvent> data,
                 SessionInfo sessionInfo,
                 int uid,
@@ -1472,7 +1432,9 @@
                 float totalConsumedPowerMah,
                 float powerComponentMah,
                 long powerComponentDurationMillis) {
-            data.add(mFrameworkStatsLogger.buildStatsEvent(
+            data.add(
+                    FrameworkStatsLog.buildStatsEvent(
+                            FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID,
                             sessionInfo.startTs(),
                             sessionInfo.endTs(),
                             sessionInfo.duration(),
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index b058bd8..504c54a 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1708,6 +1708,11 @@
                 // priority for this non-top split.
                 schedGroup = SCHED_GROUP_TOP_APP;
                 mAdjType = "resumed-split-screen-activity";
+            } else if ((flags
+                    & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) {
+                // The recently used non-top visible freeform app.
+                schedGroup = SCHED_GROUP_TOP_APP;
+                mAdjType = "perceptible-freeform-activity";
             }
             foregroundActivities = true;
             mHasVisibleActivities = true;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index fab0a56..ed22b4c 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -739,6 +739,8 @@
     // Broadcast receiver for device connections intent broadcasts
     private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
 
+    private final Executor mAudioServerLifecycleExecutor;
+
     private IMediaProjectionManager mProjectionService; // to validate projection token
 
     /** Interface for UserManagerService. */
@@ -1059,7 +1061,8 @@
                               audioserverPermissions() ?
                                 initializeAudioServerPermissionProvider(
                                     context, audioPolicyFacade, audioserverLifecycleExecutor) :
-                                    null
+                                    null,
+                              audioserverLifecycleExecutor
                               );
         }
 
@@ -1145,13 +1148,16 @@
      *               {@link AudioSystemThread} is created as the messaging thread instead.
      * @param appOps {@link AppOpsManager} system service
      * @param enforcer Used for permission enforcing
+     * @param permissionProvider Used to push permissions to audioserver
+     * @param audioserverLifecycleExecutor Used for tasks managing audioserver lifecycle
      */
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public AudioService(Context context, AudioSystemAdapter audioSystem,
             SystemServerAdapter systemServer, SettingsAdapter settings,
             AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
             @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer,
-            /* @NonNull */ AudioServerPermissionProvider permissionProvider) {
+            /* @NonNull */ AudioServerPermissionProvider permissionProvider,
+            Executor audioserverLifecycleExecutor) {
         super(enforcer);
         sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
         mContext = context;
@@ -1159,6 +1165,7 @@
         mAppOps = appOps;
 
         mPermissionProvider = permissionProvider;
+        mAudioServerLifecycleExecutor = audioserverLifecycleExecutor;
 
         mAudioSystem = audioSystem;
         mSystemServer = systemServer;
@@ -1170,6 +1177,34 @@
         mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast");
         mBroadcastHandlerThread.start();
 
+        // Listen to permission invalidations for the PermissionProvider
+        if (audioserverPermissions()) {
+            final Handler broadcastHandler = mBroadcastHandlerThread.getThreadHandler();
+            mAudioSystem.listenForSystemPropertyChange(PermissionManager.CACHE_KEY_PACKAGE_INFO,
+                    new Runnable() {
+                        // Roughly chosen to be long enough to suppress the autocork behavior
+                        // of the permission cache (50ms), and longer than the task could reasonably
+                        // take, even with many packages and users, while not introducing visible
+                        // permission leaks - since the app needs to restart, and trigger an action
+                        // which requires permissions from audioserver before this delay.
+                        // For RECORD_AUDIO, we are additionally protected by appops.
+                        final long UPDATE_DELAY_MS = 110;
+                        final AtomicLong scheduledUpdateTimestamp = new AtomicLong(0);
+                        @Override
+                        public void run() {
+                            var currentTime = SystemClock.uptimeMillis();
+                            if (currentTime > scheduledUpdateTimestamp.get()) {
+                                scheduledUpdateTimestamp.set(currentTime + UPDATE_DELAY_MS);
+                                broadcastHandler.postAtTime( () ->
+                                        mAudioServerLifecycleExecutor.execute(mPermissionProvider
+                                            ::onPermissionStateChanged),
+                                        currentTime + UPDATE_DELAY_MS
+                                    );
+                            }
+                        }
+            });
+        }
+
         mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem);
 
         mIsSingleVolume = AudioSystem.isSingleVolume(context);
@@ -11974,29 +12009,6 @@
             provider.onServiceStart(audioPolicy.getPermissionController());
         });
 
-        // Set up event listeners
-        // Must be kept in sync with PermissionManager
-        Runnable cacheSysPropHandler = new Runnable() {
-            private AtomicReference<SystemProperties.Handle> mHandle = new AtomicReference();
-            private AtomicLong mNonce = new AtomicLong();
-            @Override
-            public void run() {
-                if (mHandle.get() == null) {
-                    // Cache the handle
-                    mHandle.compareAndSet(null, SystemProperties.find(
-                            PermissionManager.CACHE_KEY_PACKAGE_INFO));
-                }
-                long nonce;
-                SystemProperties.Handle ref;
-                if ((ref = mHandle.get()) != null && (nonce = ref.getLong(0)) != 0 &&
-                        mNonce.getAndSet(nonce) != nonce) {
-                    audioserverExecutor.execute(() -> provider.onPermissionStateChanged());
-                }
-            }
-        };
-
-        SystemProperties.addChangeCallback(cacheSysPropHandler);
-
         IntentFilter packageUpdateFilter = new IntentFilter();
         packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED);
         packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED);
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 7f4bc74..d083c68 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -748,6 +748,10 @@
         return AudioSystem.setMasterMute(mute);
     }
 
+    public void listenForSystemPropertyChange(String systemPropertyName, Runnable callback) {
+        AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback);
+    }
+
     /**
      * Part of AudioService dump
      * @param pw
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 7a055d1..76b4263 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1192,6 +1192,18 @@
      */
     public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio) {
         Spline sdrToHdrSpline = mHbmData != null ? mHbmData.sdrToHdrRatioSpline : null;
+        return getHdrBrightnessFromSdr(brightness, maxDesiredHdrSdrRatio, sdrToHdrSpline);
+    }
+
+    /**
+     * Calculate the HDR brightness for the specified SDR brightenss, restricted by the
+     * maxDesiredHdrSdrRatio (the ratio between the HDR luminance and SDR luminance) and specific
+     * sdrToHdrSpline
+     *
+     * @return the HDR brightness or BRIGHTNESS_INVALID when no mapping exists.
+     */
+    public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio,
+            @Nullable Spline sdrToHdrSpline) {
         if (sdrToHdrSpline == null) {
             return PowerManager.BRIGHTNESS_INVALID;
         }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 6992580..1177be2 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -505,14 +505,15 @@
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
         mDisplayId = mLogicalDisplay.getDisplayIdLocked();
+        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
+        IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
+        DisplayDeviceInfo displayDeviceInfo = mDisplayDevice.getDisplayDeviceInfoLocked();
         mSensorManager = sensorManager;
         mHandler = new DisplayControllerHandler(handler.getLooper());
-        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceConfig();
+        mDisplayDeviceConfig = mDisplayDevice.getDisplayDeviceConfig();
         mIsEnabled = logicalDisplay.isEnabledLocked();
         mIsInTransition = logicalDisplay.isInTransitionLocked();
-        mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
+        mIsDisplayInternal = displayDeviceInfo.type == Display.TYPE_INTERNAL;
         mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
         mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
@@ -521,7 +522,7 @@
         mTag = TAG + "[" + mDisplayId + "]";
         mThermalBrightnessThrottlingDataId =
                 logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
-        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
+
         mUniqueDisplayId = mDisplayDevice.getUniqueId();
         mDisplayStatsId = mUniqueDisplayId.hashCode();
         mPhysicalDisplayName = mDisplayDevice.getNameLocked();
@@ -569,8 +570,7 @@
 
         mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
                 modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
-                mDisplayDevice.getDisplayTokenLocked(),
-                mDisplayDevice.getDisplayDeviceInfoLocked());
+                displayToken, displayDeviceInfo);
 
         mDisplayBrightnessController =
                 new DisplayBrightnessController(context, null,
@@ -584,8 +584,8 @@
                         mUniqueDisplayId,
                         mThermalBrightnessThrottlingDataId,
                         logicalDisplay.getPowerThrottlingDataIdLocked(),
-                        mDisplayDeviceConfig,
-                        mDisplayId), mContext, flags, mSensorManager);
+                        mDisplayDeviceConfig, displayDeviceInfo.width, displayDeviceInfo.height,
+                        displayToken, mDisplayId), mContext, flags, mSensorManager);
         // Seed the cached brightness
         saveBrightnessInfo(getScreenBrightnessSetting());
         mAutomaticBrightnessStrategy =
@@ -893,7 +893,7 @@
             mBrightnessClamperController.onDisplayChanged(
                     new BrightnessClamperController.DisplayDeviceData(uniqueId,
                             thermalBrightnessThrottlingDataId, powerThrottlingDataId,
-                            config, mDisplayId));
+                            config, info.width, info.height, token, mDisplayId));
 
             if (changed) {
                 updatePowerState();
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 88d2c00..afab743 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -28,13 +28,16 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Handler;
 import android.os.HandlerExecutor;
+import android.os.IBinder;
 import android.os.PowerManager;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfigInterface;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
+import android.util.Spline;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.DisplayBrightnessState;
 import com.android.server.display.DisplayDeviceConfig;
 import com.android.server.display.DisplayDeviceConfig.PowerThrottlingConfigData;
@@ -65,6 +68,11 @@
     private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers;
 
     private final List<BrightnessStateModifier> mModifiers;
+
+    private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>();
+    private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>();
+    private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState();
+
     private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener;
     private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
 
@@ -110,7 +118,16 @@
         mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags,
                 context);
         mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener,
-                data.mDisplayDeviceConfig);
+                data);
+
+        mModifiers.forEach(m -> {
+            if (m instanceof  DisplayDeviceDataListener l) {
+                mDisplayDeviceDataListeners.add(l);
+            }
+            if (m instanceof StatefulModifier s) {
+                mStatefulModifiers.add(s);
+            }
+        });
         mOnPropertiesChangedListener =
                 properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged);
         mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId());
@@ -123,6 +140,7 @@
     public void onDisplayChanged(DisplayDeviceData data) {
         mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId());
         mClampers.forEach(clamper -> clamper.onDisplayChanged(data));
+        mDisplayDeviceDataListeners.forEach(l -> l.onDisplayChanged(data));
         adjustLightSensorSubscription();
     }
 
@@ -234,14 +252,27 @@
             customAnimationRate = minClamper.getCustomAnimationRate();
         }
 
+        ModifiersAggregatedState newAggregatedState = new ModifiersAggregatedState();
+        mStatefulModifiers.forEach((clamper) -> clamper.applyStateChange(newAggregatedState));
+
         if (mBrightnessCap != brightnessCap
                 || mClamperType != clamperType
-                || mCustomAnimationRate != customAnimationRate) {
+                || mCustomAnimationRate != customAnimationRate
+                || needToNotifyExternalListener(mModifiersAggregatedState, newAggregatedState)) {
             mBrightnessCap = brightnessCap;
             mClamperType = clamperType;
             mCustomAnimationRate = customAnimationRate;
             mClamperChangeListenerExternal.onChanged();
         }
+        mModifiersAggregatedState = newAggregatedState;
+    }
+
+    private boolean needToNotifyExternalListener(ModifiersAggregatedState state1,
+            ModifiersAggregatedState state2) {
+        return !BrightnessSynchronizer.floatEquals(state1.mMaxDesiredHdrRatio,
+                state2.mMaxDesiredHdrRatio)
+                || state1.mSdrHdrRatioSpline != state2.mSdrHdrRatioSpline
+                || state1.mHdrHbmEnabled != state2.mHdrHbmEnabled;
     }
 
     private void start() {
@@ -295,17 +326,16 @@
 
         List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
                 Handler handler, ClamperChangeListener listener,
-                DisplayDeviceConfig displayDeviceConfig) {
+                DisplayDeviceData data) {
             List<BrightnessStateModifier> modifiers = new ArrayList<>();
             modifiers.add(new DisplayDimModifier(context));
             modifiers.add(new BrightnessLowPowerModeModifier());
-            if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null
-                    && displayDeviceConfig.isEvenDimmerAvailable()) {
+            if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) {
                 modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
-                        displayDeviceConfig));
+                        data.mDisplayDeviceConfig));
             }
             if (flags.useNewHdrBrightnessModifier()) {
-                modifiers.add(new HdrBrightnessModifier());
+                modifiers.add(new HdrBrightnessModifier(handler, listener, data));
             }
             return modifiers;
         }
@@ -319,7 +349,14 @@
     }
 
     /**
-     * Config Data for clampers
+     * Modifier should implement this interface in order to receive display change updates
+     */
+    interface DisplayDeviceDataListener {
+        void onDisplayChanged(DisplayDeviceData displayData);
+    }
+
+    /**
+     * Config Data for clampers/modifiers
      */
     public static class DisplayDeviceData implements BrightnessThermalClamper.ThermalData,
             BrightnessPowerClamper.PowerData,
@@ -331,23 +368,34 @@
         @NonNull
         private final String mPowerThrottlingDataId;
         @NonNull
-        private final DisplayDeviceConfig mDisplayDeviceConfig;
+        final DisplayDeviceConfig mDisplayDeviceConfig;
 
-        private final int mDisplayId;
+        final int mWidth;
+
+        final int mHeight;
+
+        final IBinder mDisplayToken;
+
+        final int mDisplayId;
 
         public DisplayDeviceData(@NonNull String uniqueDisplayId,
                 @NonNull String thermalThrottlingDataId,
                 @NonNull String powerThrottlingDataId,
                 @NonNull DisplayDeviceConfig displayDeviceConfig,
+                int width,
+                int height,
+                IBinder displayToken,
                 int displayId) {
             mUniqueDisplayId = uniqueDisplayId;
             mThermalThrottlingDataId = thermalThrottlingDataId;
             mPowerThrottlingDataId = powerThrottlingDataId;
             mDisplayDeviceConfig = displayDeviceConfig;
+            mWidth = width;
+            mHeight = height;
+            mDisplayToken = displayToken;
             mDisplayId = displayId;
         }
 
-
         @NonNull
         @Override
         public String getUniqueDisplayId() {
@@ -406,4 +454,24 @@
             return mDisplayId;
         }
     }
+
+    /**
+     * Stateful modifier should implement this interface and modify aggregatedState.
+     * AggregatedState is used by Controller to determine if updatePowerState call is needed
+     * to correctly adjust brightness
+     */
+    interface StatefulModifier {
+        void applyStateChange(ModifiersAggregatedState aggregatedState);
+    }
+
+    /**
+     * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness
+     * adjustement is needed
+     */
+    public static class ModifiersAggregatedState {
+        float mMaxDesiredHdrRatio = HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO;
+        @Nullable
+        Spline mSdrHdrRatioSpline = null;
+        boolean mHdrHbmEnabled = false;
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
index a829866..2ee70fd 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
@@ -16,17 +16,85 @@
 
 package com.android.server.display.brightness.clamper;
 
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.hardware.display.DisplayManagerInternal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.view.SurfaceControlHdrLayerInfoListener;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.config.HdrBrightnessData;
 
 import java.io.PrintWriter;
 
-public class HdrBrightnessModifier implements BrightnessStateModifier {
+public class HdrBrightnessModifier implements BrightnessStateModifier,
+        BrightnessClamperController.DisplayDeviceDataListener,
+        BrightnessClamperController.StatefulModifier {
+
+    static final float DEFAULT_MAX_HDR_SDR_RATIO = 1.0f;
+    private static final float DEFAULT_HDR_LAYER_SIZE = -1.0f;
+
+    private final SurfaceControlHdrLayerInfoListener mHdrListener =
+            new SurfaceControlHdrLayerInfoListener() {
+                @Override
+                public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers, int maxW,
+                        int maxH, int flags, float maxDesiredHdrSdrRatio) {
+                    boolean hdrLayerPresent = numberOfHdrLayers > 0;
+                    mHandler.post(() -> HdrBrightnessModifier.this.onHdrInfoChanged(
+                            hdrLayerPresent ? (float) (maxW * maxH) : DEFAULT_HDR_LAYER_SIZE,
+                            hdrLayerPresent ? maxDesiredHdrSdrRatio : DEFAULT_MAX_HDR_SDR_RATIO));
+                }
+            };
+
+    private final Handler mHandler;
+    private final BrightnessClamperController.ClamperChangeListener mClamperChangeListener;
+    private final Injector mInjector;
+
+    private IBinder mRegisteredDisplayToken;
+
+    private float mScreenSize;
+    private float mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE;
+    private HdrBrightnessData mHdrBrightnessData;
+    private DisplayDeviceConfig mDisplayDeviceConfig;
+    private float mMaxDesiredHdrRatio = DEFAULT_MAX_HDR_SDR_RATIO;
+    private Mode mMode = Mode.NO_HDR;
+
+    HdrBrightnessModifier(Handler handler,
+            BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+            BrightnessClamperController.DisplayDeviceData displayData) {
+        this(handler, clamperChangeListener, new Injector(), displayData);
+    }
+
+    @VisibleForTesting
+    HdrBrightnessModifier(Handler handler,
+            BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+            Injector injector,
+            BrightnessClamperController.DisplayDeviceData displayData) {
+        mHandler = handler;
+        mClamperChangeListener = clamperChangeListener;
+        mInjector = injector;
+        onDisplayChanged(displayData);
+    }
+
+    // Called in DisplayControllerHandler
     @Override
     public void apply(DisplayManagerInternal.DisplayPowerRequest request,
             DisplayBrightnessState.Builder stateBuilder) {
-        // noop
+        if (mHdrBrightnessData  == null) { // no hdr data
+            return;
+        }
+        if (mMode == Mode.NO_HDR) {
+            return;
+        }
+
+        float hdrBrightness = mDisplayDeviceConfig.getHdrBrightnessFromSdr(
+                stateBuilder.getBrightness(), mMaxDesiredHdrRatio,
+                mHdrBrightnessData.sdrToHdrRatioSpline);
+        stateBuilder.setHdrBrightness(hdrBrightness);
     }
 
     @Override
@@ -34,11 +102,13 @@
         // noop
     }
 
+    // Called in DisplayControllerHandler
     @Override
     public void stop() {
-        // noop
+        unregisterHdrListener();
     }
 
+
     @Override
     public boolean shouldListenToLightSensor() {
         return false;
@@ -48,4 +118,117 @@
     public void setAmbientLux(float lux) {
         // noop
     }
+
+    @Override
+    public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData displayData) {
+        mHandler.post(() -> onDisplayChanged(displayData.mDisplayToken, displayData.mWidth,
+                displayData.mHeight, displayData.mDisplayDeviceConfig));
+    }
+
+    // Called in DisplayControllerHandler
+    @Override
+    public void applyStateChange(
+            BrightnessClamperController.ModifiersAggregatedState aggregatedState) {
+        if (mMode != Mode.NO_HDR) {
+            aggregatedState.mMaxDesiredHdrRatio = mMaxDesiredHdrRatio;
+            aggregatedState.mSdrHdrRatioSpline = mHdrBrightnessData.sdrToHdrRatioSpline;
+            aggregatedState.mHdrHbmEnabled = (mMode == Mode.HBM_HDR);
+        }
+    }
+
+    // Called in DisplayControllerHandler
+    private void onDisplayChanged(IBinder displayToken, int width, int height,
+            DisplayDeviceConfig config) {
+        mDisplayDeviceConfig = config;
+        mScreenSize = (float) width * height;
+        HdrBrightnessData data = config.getHdrBrightnessData();
+        if (data == null) {
+            unregisterHdrListener();
+        } else {
+            registerHdrListener(displayToken);
+        }
+        recalculate(data, mMaxDesiredHdrRatio);
+    }
+
+    // Called in DisplayControllerHandler
+    private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) {
+        Mode newMode = recalculateMode(data);
+        // if HDR mode changed, notify changed
+        boolean needToNotifyChange = mMode != newMode;
+        // If HDR mode is active, we need to check if other HDR params are changed
+        if (mMode != HdrBrightnessModifier.Mode.NO_HDR) {
+            if (!BrightnessSynchronizer.floatEquals(mMaxDesiredHdrRatio, maxDesiredHdrRatio)
+                    || data != mHdrBrightnessData) {
+                needToNotifyChange = true;
+            }
+        }
+
+        mMode = newMode;
+        mHdrBrightnessData = data;
+        mMaxDesiredHdrRatio = maxDesiredHdrRatio;
+
+        if (needToNotifyChange) {
+            mClamperChangeListener.onChanged();
+        }
+    }
+
+    // Called in DisplayControllerHandler
+    private Mode recalculateMode(@Nullable HdrBrightnessData data) {
+        // no config
+        if (data == null) {
+            return Mode.NO_HDR;
+        }
+        // HDR layer < minHdr % for Nbm
+        if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) {
+            return Mode.NO_HDR;
+        }
+        // HDR layer < minHdr % for Hbm, and HDR layer >= that minHdr % for Nbm
+        if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForHbm) {
+            return Mode.NBM_HDR;
+        }
+        // HDR layer > that minHdr % for Hbm
+        return Mode.HBM_HDR;
+    }
+
+    // Called in DisplayControllerHandler
+    private void onHdrInfoChanged(float hdrLayerSize, float maxDesiredHdrSdrRatio) {
+        mHdrLayerSize = hdrLayerSize;
+        recalculate(mHdrBrightnessData, maxDesiredHdrSdrRatio);
+    }
+
+    // Called in DisplayControllerHandler
+    private void registerHdrListener(IBinder displayToken) {
+        if (mRegisteredDisplayToken == displayToken) {
+            return;
+        }
+        unregisterHdrListener();
+        if (displayToken != null) {
+            mInjector.registerHdrListener(mHdrListener, displayToken);
+            mRegisteredDisplayToken = displayToken;
+        }
+    }
+
+    // Called in DisplayControllerHandler
+    private void unregisterHdrListener() {
+        if (mRegisteredDisplayToken != null) {
+            mInjector.unregisterHdrListener(mHdrListener, mRegisteredDisplayToken);
+            mRegisteredDisplayToken = null;
+            mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE;
+        }
+    }
+
+    private enum Mode {
+        NO_HDR, NBM_HDR, HBM_HDR
+    }
+
+    @SuppressLint("MissingPermission")
+    static class Injector {
+        void registerHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
+            listener.register(token);
+        }
+
+        void unregisterHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
+            listener.unregister(token);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 1f46af8..bb2efa1 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2224,12 +2224,6 @@
 
     // Native callback.
     @SuppressWarnings("unused")
-    private void notifyConfigurationChanged(long whenNanos) {
-        mWindowManagerCallbacks.notifyConfigurationChanged();
-    }
-
-    // Native callback.
-    @SuppressWarnings("unused")
     private void notifyInputDevicesChanged(InputDevice[] inputDevices) {
         synchronized (mInputDevicesLock) {
             if (!mInputDevicesChangedPending) {
@@ -2240,6 +2234,9 @@
 
             mInputDevices = inputDevices;
         }
+        // Input device change can possibly change configuration, so notify window manager to update
+        // its configuration.
+        mWindowManagerCallbacks.notifyConfigurationChanged();
     }
 
     // Native callback.
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 96ca570..5d10780 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -702,24 +702,9 @@
             super(true);
         }
 
-        @GuardedBy("ImfLock.class")
-        private boolean isChangingPackagesOfCurrentUserLocked() {
-            final int userId = getChangingUserId();
-            final boolean retval = userId == mCurrentUserId;
-            if (DEBUG) {
-                if (!retval) {
-                    Slog.d(TAG, "--- ignore this call back from a background user: " + userId);
-                }
-            }
-            return retval;
-        }
-
         @Override
         public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
             synchronized (ImfLock.class) {
-                if (!isChangingPackagesOfCurrentUserLocked()) {
-                    return false;
-                }
                 final int userId = getChangingUserId();
                 final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
                 String curInputMethodId = settings.getSelectedInputMethod();
@@ -1114,13 +1099,9 @@
             final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext,
                     userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO);
             InputMethodSettingsRepository.put(userId, newSettings);
-            if (mCurrentUserId == userId) {
-                // We need to rebuild IMEs.
-                postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
-                updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
-            } else if (mConcurrentMultiUserModeEnabled) {
-                initializeVisibleBackgroundUserLocked(userId);
-            }
+            // We need to rebuild IMEs.
+            postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
+            updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
         }
     }
 
@@ -1455,9 +1436,9 @@
                 (windowToken, imeVisible) -> {
                     if (Flags.refactorInsetsController()) {
                         if (imeVisible) {
-                            showSoftInputInternal(windowToken);
+                            showCurrentInputInternal(windowToken);
                         } else {
-                            hideSoftInputInternal(windowToken);
+                            hideCurrentInputInternal(windowToken);
                         }
                     }
                 });
@@ -1933,7 +1914,7 @@
         if (Flags.refactorInsetsController()) {
             if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null
                     && userData.mImeBindingState.mFocusedWindow != null) {
-                showSoftInputInternal(userData.mImeBindingState.mFocusedWindow);
+                showCurrentInputInternal(userData.mImeBindingState.mFocusedWindow);
             }
         } else {
             if (isShowRequestedForCurrentWindow(userId)) {
@@ -3165,8 +3146,8 @@
         }
     }
 
-    boolean showSoftInputInternal(IBinder windowToken) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInputInternal");
+    boolean showCurrentInputInternal(IBinder windowToken) {
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal");
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#showSoftInput", mDumper);
         synchronized (ImfLock.class) {
@@ -3185,8 +3166,8 @@
         }
     }
 
-    boolean hideSoftInputInternal(IBinder windowToken) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInputInternal");
+    boolean hideCurrentInputInternal(IBinder windowToken) {
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal");
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#hideSoftInput", mDumper);
         synchronized (ImfLock.class) {
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index a821f545..c4d601d 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -67,19 +67,13 @@
     private static final String TAG_SIGNATURE = "signature";
     private static final String TAG_FALLBACK = "isFallback";
     private static final String PIN_GROUP = "webview";
+
+    private final Context mContext;
     private final WebViewProviderInfo[] mWebViewProviderPackages;
 
-    // Initialization-on-demand holder idiom for getting the WebView provider packages once and
-    // for all in a thread-safe manner.
-    private static class LazyHolder {
-        private static final SystemImpl INSTANCE = new SystemImpl();
-    }
+    SystemImpl(Context context) {
+        mContext = context;
 
-    public static SystemImpl getInstance() {
-        return LazyHolder.INSTANCE;
-    }
-
-    private SystemImpl() {
         int numFallbackPackages = 0;
         int numAvailableByDefaultPackages = 0;
         XmlResourceParser parser = null;
@@ -184,14 +178,14 @@
     }
 
     @Override
-    public String getUserChosenWebViewProvider(Context context) {
-        return Settings.Global.getString(context.getContentResolver(),
+    public String getUserChosenWebViewProvider() {
+        return Settings.Global.getString(mContext.getContentResolver(),
                 Settings.Global.WEBVIEW_PROVIDER);
     }
 
     @Override
-    public void updateUserSetting(Context context, String newProviderName) {
-        Settings.Global.putString(context.getContentResolver(),
+    public void updateUserSetting(String newProviderName) {
+        Settings.Global.putString(mContext.getContentResolver(),
                 Settings.Global.WEBVIEW_PROVIDER,
                 newProviderName == null ? "" : newProviderName);
     }
@@ -207,8 +201,8 @@
     }
 
     @Override
-    public void enablePackageForAllUsers(Context context, String packageName, boolean enable) {
-        UserManager userManager = (UserManager)context.getSystemService(Context.USER_SERVICE);
+    public void enablePackageForAllUsers(String packageName, boolean enable) {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         for(UserInfo userInfo : userManager.getUsers()) {
             enablePackageForUser(packageName, enable, userInfo.id);
         }
@@ -228,16 +222,15 @@
     }
 
     @Override
-    public void installExistingPackageForAllUsers(Context context, String packageName) {
-        UserManager userManager = context.getSystemService(UserManager.class);
+    public void installExistingPackageForAllUsers(String packageName) {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         for (UserInfo userInfo : userManager.getUsers()) {
             installPackageForUser(packageName, userInfo.id);
         }
     }
 
     private void installPackageForUser(String packageName, int userId) {
-        final Context context = AppGlobals.getInitialApplication();
-        final Context contextAsUser = context.createContextAsUser(UserHandle.of(userId), 0);
+        final Context contextAsUser = mContext.createContextAsUser(UserHandle.of(userId), 0);
         final PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller();
         installer.installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN, null);
     }
@@ -255,29 +248,28 @@
     }
 
     @Override
-    public List<UserPackage> getPackageInfoForProviderAllUsers(Context context,
-            WebViewProviderInfo configInfo) {
-        return UserPackage.getPackageInfosAllUsers(context, configInfo.packageName, PACKAGE_FLAGS);
+    public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo) {
+        return UserPackage.getPackageInfosAllUsers(mContext, configInfo.packageName, PACKAGE_FLAGS);
     }
 
     @Override
-    public int getMultiProcessSetting(Context context) {
+    public int getMultiProcessSetting() {
         if (updateServiceV2()) {
             throw new IllegalStateException(
                     "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
         }
         return Settings.Global.getInt(
-                context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0);
+                mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0);
     }
 
     @Override
-    public void setMultiProcessSetting(Context context, int value) {
+    public void setMultiProcessSetting(int value) {
         if (updateServiceV2()) {
             throw new IllegalStateException(
                     "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
         }
         Settings.Global.putInt(
-                context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value);
+                mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java
index ad32f62..3b77d07 100644
--- a/services/core/java/com/android/server/webkit/SystemInterface.java
+++ b/services/core/java/com/android/server/webkit/SystemInterface.java
@@ -16,7 +16,6 @@
 
 package com.android.server.webkit;
 
-import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -34,19 +33,19 @@
  * @hide
  */
 public interface SystemInterface {
-    public WebViewProviderInfo[] getWebViewPackages();
-    public int onWebViewProviderChanged(PackageInfo packageInfo);
-    public long getFactoryPackageVersion(String packageName) throws NameNotFoundException;
+    WebViewProviderInfo[] getWebViewPackages();
+    int onWebViewProviderChanged(PackageInfo packageInfo);
+    long getFactoryPackageVersion(String packageName) throws NameNotFoundException;
 
-    public String getUserChosenWebViewProvider(Context context);
-    public void updateUserSetting(Context context, String newProviderName);
-    public void killPackageDependents(String packageName);
+    String getUserChosenWebViewProvider();
+    void updateUserSetting(String newProviderName);
+    void killPackageDependents(String packageName);
 
-    public void enablePackageForAllUsers(Context context, String packageName, boolean enable);
-    public void installExistingPackageForAllUsers(Context context, String packageName);
+    void enablePackageForAllUsers(String packageName, boolean enable);
+    void installExistingPackageForAllUsers(String packageName);
 
-    public boolean systemIsDebuggable();
-    public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo)
+    boolean systemIsDebuggable();
+    PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo)
             throws NameNotFoundException;
     /**
      * Get the PackageInfos of all users for the package represented by {@param configInfo}.
@@ -54,15 +53,14 @@
      *         certain user. The returned array can contain null PackageInfos if the given package
      *         is uninstalled for some user.
      */
-    public List<UserPackage> getPackageInfoForProviderAllUsers(Context context,
-            WebViewProviderInfo configInfo);
+    List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo);
 
-    public int getMultiProcessSetting(Context context);
-    public void setMultiProcessSetting(Context context, int value);
-    public void notifyZygote(boolean enableMultiProcess);
+    int getMultiProcessSetting();
+    void setMultiProcessSetting(int value);
+    void notifyZygote(boolean enableMultiProcess);
     /** Start the zygote if it's not already running. */
-    public void ensureZygoteStarted();
-    public boolean isMultiProcessDefaultEnabled();
+    void ensureZygoteStarted();
+    boolean isMultiProcessDefaultEnabled();
 
-    public void pinWebviewIfRequired(ApplicationInfo appInfo);
+    void pinWebviewIfRequired(ApplicationInfo appInfo);
 }
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index 043470f..7acb864 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -73,9 +73,9 @@
     public WebViewUpdateService(Context context) {
         super(context);
         if (updateServiceV2()) {
-            mImpl = new WebViewUpdateServiceImpl2(context, SystemImpl.getInstance());
+            mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context));
         } else {
-            mImpl = new WebViewUpdateServiceImpl(context, SystemImpl.getInstance());
+            mImpl = new WebViewUpdateServiceImpl(new SystemImpl(context));
         }
     }
 
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
index dcf20f9..b9be4a2 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
@@ -16,7 +16,6 @@
 package com.android.server.webkit;
 
 import android.annotation.Nullable;
-import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.Signature;
@@ -92,7 +91,6 @@
     private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE;
 
     private final SystemInterface mSystemInterface;
-    private final Context mContext;
 
     private long mMinimumVersionCode = -1;
 
@@ -110,8 +108,7 @@
 
     private final Object mLock = new Object();
 
-    WebViewUpdateServiceImpl(Context context, SystemInterface systemInterface) {
-        mContext = context;
+    WebViewUpdateServiceImpl(SystemInterface systemInterface) {
         mSystemInterface = systemInterface;
     }
 
@@ -173,7 +170,7 @@
         try {
             synchronized (mLock) {
                 mCurrentWebViewPackage = findPreferredWebViewPackage();
-                String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext);
+                String userSetting = mSystemInterface.getUserChosenWebViewProvider();
                 if (userSetting != null
                         && !userSetting.equals(mCurrentWebViewPackage.packageName)) {
                     // Don't persist the user-chosen setting across boots if the package being
@@ -181,8 +178,7 @@
                     // be surprised by the device switching to using a certain webview package,
                     // that was uninstalled/disabled a long time ago, if it is installed/enabled
                     // again.
-                    mSystemInterface.updateUserSetting(mContext,
-                            mCurrentWebViewPackage.packageName);
+                    mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName);
                 }
                 onWebViewProviderChanged(mCurrentWebViewPackage);
             }
@@ -203,8 +199,7 @@
             WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
             if (fallbackProvider != null) {
                 Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName);
-                mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName,
-                                                          true);
+                mSystemInterface.enablePackageForAllUsers(fallbackProvider.packageName, true);
             } else {
                 Slog.e(TAG, "No valid provider and no fallback available.");
             }
@@ -316,7 +311,7 @@
             oldPackage = mCurrentWebViewPackage;
 
             if (newProviderName != null) {
-                mSystemInterface.updateUserSetting(mContext, newProviderName);
+                mSystemInterface.updateUserSetting(newProviderName);
             }
 
             try {
@@ -447,7 +442,7 @@
     private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
         ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos();
 
-        String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext);
+        String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider();
 
         // If the user has chosen provider, use that (if it's installed and enabled for all
         // users).
@@ -455,7 +450,7 @@
             if (providerAndPackage.provider.packageName.equals(userChosenProvider)) {
                 // userPackages can contain null objects.
                 List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
+                        mSystemInterface.getPackageInfoForProviderAllUsers(
                                 providerAndPackage.provider);
                 if (isInstalledAndEnabledForAllUsers(userPackages)) {
                     return providerAndPackage.packageInfo;
@@ -470,7 +465,7 @@
             if (providerAndPackage.provider.availableByDefault) {
                 // userPackages can contain null objects.
                 List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
+                        mSystemInterface.getPackageInfoForProviderAllUsers(
                                 providerAndPackage.provider);
                 if (isInstalledAndEnabledForAllUsers(userPackages)) {
                     return providerAndPackage.packageInfo;
@@ -658,7 +653,7 @@
 
     @Override
     public boolean isMultiProcessEnabled() {
-        int settingValue = mSystemInterface.getMultiProcessSetting(mContext);
+        int settingValue = mSystemInterface.getMultiProcessSetting();
         if (mSystemInterface.isMultiProcessDefaultEnabled()) {
             // Multiprocess should be enabled unless the user has turned it off manually.
             return settingValue > MULTIPROCESS_SETTING_OFF_VALUE;
@@ -671,7 +666,7 @@
     @Override
     public void enableMultiProcess(boolean enable) {
         PackageInfo current = getCurrentWebViewPackage();
-        mSystemInterface.setMultiProcessSetting(mContext,
+        mSystemInterface.setMultiProcessSetting(
                 enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE);
         mSystemInterface.notifyZygote(enable);
         if (current != null) {
@@ -725,7 +720,7 @@
         pw.println("  WebView packages:");
         for (WebViewProviderInfo provider : allProviders) {
             List<UserPackage> userPackages =
-                    mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider);
+                    mSystemInterface.getPackageInfoForProviderAllUsers(provider);
             PackageInfo systemUserPackageInfo =
                     userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo();
             if (systemUserPackageInfo == null) {
@@ -741,7 +736,7 @@
                     systemUserPackageInfo.applicationInfo.targetSdkVersion);
             if (validity == VALIDITY_OK) {
                 boolean installedForAllUsers = isInstalledAndEnabledForAllUsers(
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider));
+                        mSystemInterface.getPackageInfoForProviderAllUsers(provider));
                 pw.println(String.format(
                         "    Valid package %s (%s) is %s installed/enabled for all users",
                         systemUserPackageInfo.packageName,
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 993597e..307c15b 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -16,7 +16,6 @@
 package com.android.server.webkit;
 
 import android.annotation.Nullable;
-import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.Signature;
@@ -86,7 +85,6 @@
     private static final int VALIDITY_NO_LIBRARY_FLAG = 4;
 
     private final SystemInterface mSystemInterface;
-    private final Context mContext;
     private final WebViewProviderInfo mDefaultProvider;
 
     private long mMinimumVersionCode = -1;
@@ -108,8 +106,7 @@
 
     private final Object mLock = new Object();
 
-    WebViewUpdateServiceImpl2(Context context, SystemInterface systemInterface) {
-        mContext = context;
+    WebViewUpdateServiceImpl2(SystemInterface systemInterface) {
         mSystemInterface = systemInterface;
         WebViewProviderInfo[] webviewProviders = getWebViewPackages();
 
@@ -194,8 +191,7 @@
         }
         if (mCurrentWebViewPackage.packageName.equals(mDefaultProvider.packageName)) {
             List<UserPackage> userPackages =
-                    mSystemInterface.getPackageInfoForProviderAllUsers(
-                            mContext, mDefaultProvider);
+                    mSystemInterface.getPackageInfoForProviderAllUsers(mDefaultProvider);
             return !isInstalledAndEnabledForAllUsers(userPackages);
         } else {
             return false;
@@ -216,10 +212,8 @@
                 TAG,
                 "No provider available for all users, trying to install and enable "
                         + mDefaultProvider.packageName);
-        mSystemInterface.installExistingPackageForAllUsers(
-                mContext, mDefaultProvider.packageName);
-        mSystemInterface.enablePackageForAllUsers(
-                mContext, mDefaultProvider.packageName, true);
+        mSystemInterface.installExistingPackageForAllUsers(mDefaultProvider.packageName);
+        mSystemInterface.enablePackageForAllUsers(mDefaultProvider.packageName, true);
     }
 
     @Override
@@ -229,7 +223,7 @@
             synchronized (mLock) {
                 mCurrentWebViewPackage = findPreferredWebViewPackage();
                 repairNeeded = shouldTriggerRepairLocked();
-                String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext);
+                String userSetting = mSystemInterface.getUserChosenWebViewProvider();
                 if (userSetting != null
                         && !userSetting.equals(mCurrentWebViewPackage.packageName)) {
                     // Don't persist the user-chosen setting across boots if the package being
@@ -237,8 +231,7 @@
                     // be surprised by the device switching to using a certain webview package,
                     // that was uninstalled/disabled a long time ago, if it is installed/enabled
                     // again.
-                    mSystemInterface.updateUserSetting(mContext,
-                            mCurrentWebViewPackage.packageName);
+                    mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName);
                 }
                 onWebViewProviderChanged(mCurrentWebViewPackage);
             }
@@ -362,7 +355,7 @@
             oldPackage = mCurrentWebViewPackage;
 
             if (newProviderName != null) {
-                mSystemInterface.updateUserSetting(mContext, newProviderName);
+                mSystemInterface.updateUserSetting(newProviderName);
             }
 
             try {
@@ -493,7 +486,7 @@
         Counter.logIncrement("webview.value_find_preferred_webview_package_counter");
         // If the user has chosen provider, use that (if it's installed and enabled for all
         // users).
-        String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext);
+        String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider();
         WebViewProviderInfo userChosenProvider =
                 getWebViewProviderForPackage(userChosenPackageName);
         if (userChosenProvider != null) {
@@ -502,8 +495,7 @@
                         mSystemInterface.getPackageInfoForProvider(userChosenProvider);
                 if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) {
                     List<UserPackage> userPackages =
-                            mSystemInterface.getPackageInfoForProviderAllUsers(
-                                    mContext, userChosenProvider);
+                            mSystemInterface.getPackageInfoForProviderAllUsers(userChosenProvider);
                     if (isInstalledAndEnabledForAllUsers(userPackages)) {
                         return packageInfo;
                     }
@@ -779,7 +771,7 @@
         pw.println("  WebView packages:");
         for (WebViewProviderInfo provider : allProviders) {
             List<UserPackage> userPackages =
-                    mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider);
+                    mSystemInterface.getPackageInfoForProviderAllUsers(provider);
             PackageInfo systemUserPackageInfo =
                     userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo();
             if (systemUserPackageInfo == null) {
@@ -798,8 +790,7 @@
             if (validity == VALIDITY_OK) {
                 boolean installedForAllUsers =
                         isInstalledAndEnabledForAllUsers(
-                                mSystemInterface.getPackageInfoForProviderAllUsers(
-                                        mContext, provider));
+                                mSystemInterface.getPackageInfoForProviderAllUsers(provider));
                 pw.println(
                         TextUtils.formatSimple(
                                 "    Valid package %s (%s) is %s installed/enabled for all users",
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index fa6ac65..400919a 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2867,7 +2867,10 @@
         if (mStartingData != null) {
             if (mStartingData.mAssociatedTask != null) {
                 // The snapshot type may have called associateStartingDataWithTask().
-                attachStartingSurfaceToAssociatedTask();
+                // If this activity is rotated, don't attach to task to preserve the transform.
+                if (!hasFixedRotationTransform()) {
+                    attachStartingSurfaceToAssociatedTask();
+                }
             } else if (isEmbedded()) {
                 associateStartingWindowWithTaskIfNeeded();
             }
@@ -2898,6 +2901,12 @@
                 || mStartingData.mAssociatedTask != null) {
             return;
         }
+        if (task.isVisible() && !task.inTransition()) {
+            // Don't associated with task if the task is visible especially when the activity is
+            // embedded. We just need to show splash screen on the activity in case the first frame
+            // is not ready.
+            return;
+        }
         associateStartingDataWithTask();
         attachStartingSurfaceToAssociatedTask();
     }
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index d9f11b1..05d4c82 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -226,6 +226,14 @@
                         : getDefaultMinAspectRatio();
     }
 
+    float getDefaultMinAspectRatioForUnresizableAppsFromConfig() {
+        return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps();
+    }
+
+    boolean isSplitScreenAspectRatioForUnresizableAppsEnabled() {
+        return mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+    }
+
     private float getDisplaySizeMinAspectRatio() {
         final DisplayArea displayArea = mActivityRecord.getDisplayArea();
         if (displayArea == null) {
@@ -278,7 +286,7 @@
         return getSplitScreenAspectRatio();
     }
 
-    private float getDefaultMinAspectRatio() {
+    float getDefaultMinAspectRatio() {
         if (mActivityRecord.getDisplayArea() == null
                 || !mAppCompatConfiguration
                 .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
index f9f5058..3ecdff6 100644
--- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -16,18 +16,33 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
+import static android.content.pm.ActivityInfo.isFixedOrientationPortrait;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.AppCompatUtils.computeAspectRatio;
 import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity;
 import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
+import android.app.AppCompatTaskInfo;
+import android.app.TaskInfo;
 import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.SystemProperties;
 import android.util.Size;
 import android.view.Gravity;
 
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
 import java.util.function.Consumer;
 
 /**
@@ -38,6 +53,8 @@
 
     public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
             .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
+    public static final int DESKTOP_MODE_LANDSCAPE_APP_PADDING = SystemProperties
+            .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25);
 
     /**
      * Updates launch bounds for an activity with respect to its activity options, window layout,
@@ -48,12 +65,8 @@
             @NonNull Rect outBounds, @NonNull Consumer<String> logger) {
         // Use stable frame instead of raw frame to avoid launching freeform windows on top of
         // stable insets, which usually are system widgets such as sysbar & navbar.
-        final TaskDisplayArea displayArea = task.getDisplayArea();
-        final Rect screenBounds = displayArea.getBounds();
         final Rect stableBounds = new Rect();
-        displayArea.getStableRect(stableBounds);
-        final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        task.getDisplayArea().getStableRect(stableBounds);
 
         if (options != null && options.getLaunchBounds() != null) {
             outBounds.set(options.getLaunchBounds());
@@ -63,37 +76,282 @@
             final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
             if (layout.hasSpecifiedSize()) {
                 calculateLayoutBounds(stableBounds, layout, outBounds,
-                        new Size(desiredWidth, desiredHeight));
+                        calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
                 applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
                         stableBounds);
                 logger.accept("layout specifies sizes, inheriting size and applying gravity");
             } else if (verticalGravity > 0 || horizontalGravity > 0) {
-                calculateAndCentreInitialBounds(outBounds, screenBounds);
+                outBounds.set(calculateInitialBounds(task, activity, stableBounds));
                 applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
                         stableBounds);
                 logger.accept("layout specifies gravity, applying desired bounds and gravity");
             }
         } else {
-            calculateAndCentreInitialBounds(outBounds, screenBounds);
+            outBounds.set(calculateInitialBounds(task, activity, stableBounds));
             logger.accept("layout not specified, applying desired bounds");
         }
     }
 
     /**
-     * Calculates the initial height and width of a task in desktop mode and centers it within the
-     * window bounds.
+     * Calculates the initial bounds required for an application to fill a scale of the display
+     * bounds without any letterboxing. This is done by taking into account the applications
+     * fullscreen size, aspect ratio, orientation and resizability to calculate an area this is
+     * compatible with the applications previous configuration.
      */
-    private static void calculateAndCentreInitialBounds(@NonNull Rect outBounds,
+    private static @NonNull Rect calculateInitialBounds(@NonNull Task task,
+            @NonNull ActivityRecord activity, @NonNull Rect stableBounds
+    ) {
+        final TaskInfo taskInfo = task.getTaskInfo();
+        // Display bounds not taking into account insets.
+        final TaskDisplayArea displayArea = task.getDisplayArea();
+        final Rect screenBounds = displayArea.getBounds();
+        final Size idealSize = calculateIdealSize(screenBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        if (!Flags.enableWindowingDynamicInitialBounds()) {
+            return centerInScreen(idealSize, screenBounds);
+        }
+        // TODO(b/353457301): Replace with app compat aspect ratio method when refactoring complete.
+        float appAspectRatio = calculateAspectRatio(task, activity);
+        final float tdaWidth = stableBounds.width();
+        final float tdaHeight = stableBounds.height();
+        final int activityOrientation = activity.getOverrideOrientation();
+        final Size initialSize = switch (taskInfo.configuration.orientation) {
+            case ORIENTATION_LANDSCAPE -> {
+                // Device in landscape orientation.
+                if (appAspectRatio == 0) {
+                    appAspectRatio = 1;
+                }
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationPortrait(activityOrientation)) {
+                        // For portrait resizeable activities, respect apps fullscreen width but
+                        // apply ideal size height.
+                        yield new Size((int) ((tdaHeight / appAspectRatio) + 0.5f),
+                                idealSize.getHeight());
+                    }
+                    // For landscape resizeable activities, simply apply ideal size.
+                    yield idealSize;
+                }
+                // If activity is unresizeable, regardless of orientation, calculate maximum size
+                // (within the ideal size) maintaining original aspect ratio.
+                yield maximizeSizeGivenAspectRatio(
+                        activity.getOverrideOrientation(), idealSize, appAspectRatio);
+            }
+            case ORIENTATION_PORTRAIT -> {
+                // Device in portrait orientation.
+                final int customPortraitWidthForLandscapeApp = screenBounds.width()
+                        - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationLandscape(activityOrientation)) {
+                        if (appAspectRatio == 0) {
+                            appAspectRatio = tdaWidth / (tdaWidth - 1);
+                        }
+                        // For landscape resizeable activities, respect apps fullscreen height and
+                        // apply custom app width.
+                        yield new Size(customPortraitWidthForLandscapeApp,
+                                (int) ((tdaWidth / appAspectRatio) + 0.5f));
+                    }
+                    // For portrait resizeable activities, simply apply ideal size.
+                    yield idealSize;
+                }
+                if (appAspectRatio == 0) {
+                    appAspectRatio = 1;
+                }
+                if (isFixedOrientationLandscape(activityOrientation)) {
+                    // For landscape unresizeable activities, apply custom app width to ideal size
+                    // and calculate maximum size with this area while maintaining original aspect
+                    // ratio.
+                    yield maximizeSizeGivenAspectRatio(activityOrientation,
+                            new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()),
+                            appAspectRatio);
+                }
+                // For portrait unresizeable activities, calculate maximum size (within the ideal
+                // size) maintaining original aspect ratio.
+                yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio);
+            }
+            default -> idealSize;
+        };
+        return centerInScreen(initialSize, screenBounds);
+    }
+
+    /**
+     * Calculates the largest size that can fit in a given area while maintaining a specific aspect
+     * ratio.
+     */
+    private static @NonNull Size maximizeSizeGivenAspectRatio(
+            @ActivityInfo.ScreenOrientation int orientation,
+            @NonNull Size targetArea,
+            float aspectRatio
+    ) {
+        final int targetHeight = targetArea.getHeight();
+        final int targetWidth = targetArea.getWidth();
+        final int finalHeight;
+        final int finalWidth;
+        if (isFixedOrientationPortrait(orientation)) {
+            // Portrait activity.
+            // Calculate required width given ideal height and aspect ratio.
+            int tempWidth = (int) (targetHeight / aspectRatio);
+            if (tempWidth <= targetWidth) {
+                // If the calculated width does not exceed the ideal width, overall size is within
+                // ideal size and can be applied.
+                finalHeight = targetHeight;
+                finalWidth = tempWidth;
+            } else {
+                // Applying target height cause overall size to exceed ideal size when maintain
+                // aspect ratio. Instead apply ideal width and calculate required height to respect
+                // aspect ratio.
+                finalWidth = targetWidth;
+                finalHeight = (int) (finalWidth * aspectRatio);
+            }
+        } else {
+            // Landscape activity.
+            // Calculate required width given ideal height and aspect ratio.
+            int tempWidth = (int) (targetHeight * aspectRatio);
+            if (tempWidth <= targetWidth) {
+                // If the calculated width does not exceed the ideal width, overall size is within
+                // ideal size and can be applied.
+                finalHeight = targetHeight;
+                finalWidth = tempWidth;
+            } else {
+                // Applying target height cause overall size to exceed ideal size when maintain
+                // aspect ratio. Instead apply ideal width and calculate required height to respect
+                // aspect ratio.
+                finalWidth = targetWidth;
+                finalHeight = (int) (finalWidth / aspectRatio);
+            }
+        }
+        return new Size(finalWidth, finalHeight);
+    }
+
+    /**
+     * Calculates the aspect ratio of an activity from its fullscreen bounds.
+     */
+    @VisibleForTesting
+    static float calculateAspectRatio(@NonNull Task task, @NonNull ActivityRecord activity) {
+        final TaskInfo taskInfo = task.getTaskInfo();
+        final float fullscreenWidth = task.getDisplayArea().getBounds().width();
+        final float fullscreenHeight = task.getDisplayArea().getBounds().height();
+        final float maxAspectRatio = activity.getMaxAspectRatio();
+        final float minAspectRatio = activity.getMinAspectRatio();
+        float desiredAspectRatio = 0;
+        if (taskInfo.isRunning) {
+            final AppCompatTaskInfo appCompatTaskInfo =  taskInfo.appCompatTaskInfo;
+            if (appCompatTaskInfo.topActivityBoundsLetterboxed) {
+                desiredAspectRatio = (float) Math.max(
+                        appCompatTaskInfo.topActivityLetterboxWidth,
+                        appCompatTaskInfo.topActivityLetterboxHeight)
+                        / Math.min(appCompatTaskInfo.topActivityLetterboxWidth,
+                        appCompatTaskInfo.topActivityLetterboxHeight);
+            } else {
+                desiredAspectRatio = Math.max(fullscreenHeight, fullscreenWidth)
+                        / Math.min(fullscreenHeight, fullscreenWidth);
+            }
+        } else {
+            final float letterboxAspectRatioOverride =
+                    getFixedOrientationLetterboxAspectRatio(activity, task);
+            if (!task.mDisplayContent.getIgnoreOrientationRequest()) {
+                desiredAspectRatio = DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+            } else if (letterboxAspectRatioOverride
+                    > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+                desiredAspectRatio = letterboxAspectRatioOverride;
+            }
+        }
+        // If the activity matches display orientation, the display aspect ratio should be used
+        if (activityMatchesDisplayOrientation(
+                taskInfo.configuration.orientation,
+                activity.getOverrideOrientation())) {
+            desiredAspectRatio = Math.max(fullscreenWidth, fullscreenHeight)
+                    / Math.min(fullscreenWidth, fullscreenHeight);
+        }
+        if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) {
+            desiredAspectRatio = maxAspectRatio;
+        } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) {
+            desiredAspectRatio = minAspectRatio;
+        }
+        return desiredAspectRatio;
+    }
+
+    private static boolean activityMatchesDisplayOrientation(
+            @Configuration.Orientation int deviceOrientation,
+            @ActivityInfo.ScreenOrientation int activityOrientation) {
+        if (deviceOrientation == ORIENTATION_PORTRAIT) {
+            return isFixedOrientationPortrait(activityOrientation);
+        }
+        return isFixedOrientationLandscape(activityOrientation);
+    }
+
+    /**
+     * Calculates the desired initial bounds for applications in desktop windowing. This is done as
+     * a scale of the screen bounds.
+     */
+    private static @NonNull Size calculateIdealSize(@NonNull Rect screenBounds, float scale) {
+        final int width = (int) (screenBounds.width() * scale);
+        final int height = (int) (screenBounds.height() * scale);
+        return new Size(width, height);
+    }
+
+    /**
+     * Adjusts bounds to be positioned in the middle of the screen.
+     */
+    private static @NonNull Rect centerInScreen(@NonNull Size desiredSize,
             @NonNull Rect screenBounds) {
-        // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
-        // The desired dimensions that a fully resizable window should take when initially entering
-        // desktop mode. Calculated as a percentage of the available display area as defined by the
-        // DESKTOP_MODE_INITIAL_BOUNDS_SCALE.
-        final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        outBounds.right = desiredWidth;
-        outBounds.bottom = desiredHeight;
-        outBounds.offset(screenBounds.centerX() - outBounds.centerX(),
-                screenBounds.centerY() - outBounds.centerY());
+        // TODO(b/325240051): Position apps with bottom heavy offset
+        final int heightOffset = (screenBounds.height() - desiredSize.getHeight()) / 2;
+        final int widthOffset = (screenBounds.width() - desiredSize.getWidth()) / 2;
+        final Rect resultBounds = new Rect(0, 0,
+                desiredSize.getWidth(), desiredSize.getHeight());
+        resultBounds.offset(screenBounds.left + widthOffset, screenBounds.top + heightOffset);
+        return resultBounds;
+    }
+
+    private static float getFixedOrientationLetterboxAspectRatio(@NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        return activity.shouldCreateCompatDisplayInsets()
+                ? getDefaultMinAspectRatioForUnresizableApps(activity, task)
+                : activity.mAppCompatController.getAppCompatAspectRatioOverrides()
+                        .getDefaultMinAspectRatio();
+    }
+
+    private static float getDefaultMinAspectRatioForUnresizableApps(
+            @NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        final AppCompatAspectRatioOverrides appCompatAspectRatioOverrides =
+                activity.mAppCompatController.getAppCompatAspectRatioOverrides();
+        if (appCompatAspectRatioOverrides.isSplitScreenAspectRatioForUnresizableAppsEnabled()) {
+            // Default letterbox aspect ratio for unresizable apps.
+            return getSplitScreenAspectRatio(activity, task);
+        }
+
+        if (appCompatAspectRatioOverrides.getDefaultMinAspectRatioForUnresizableAppsFromConfig()
+                > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+            return appCompatAspectRatioOverrides
+                    .getDefaultMinAspectRatioForUnresizableAppsFromConfig();
+        }
+
+        return appCompatAspectRatioOverrides.getDefaultMinAspectRatio();
+    }
+
+    /**
+     * Calculates the aspect ratio of the available display area when an app enters split-screen on
+     * a given device, taking into account any dividers and insets.
+     */
+    private static float getSplitScreenAspectRatio(@NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        final int dividerWindowWidth =
+                activity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        R.dimen.docked_stack_divider_thickness);
+        final int dividerInsets =
+                activity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        R.dimen.docked_stack_divider_insets);
+        final int dividerSize = dividerWindowWidth - dividerInsets * 2;
+        final Rect bounds = new Rect(0, 0,
+                task.mDisplayContent.getDisplayInfo().appWidth,
+                task.mDisplayContent.getDisplayInfo().appHeight);
+        if (bounds.width() >= bounds.height()) {
+            bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
+            bounds.right = bounds.centerX();
+        } else {
+            bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
+            bounds.bottom = bounds.centerY();
+        }
+        return computeAspectRatio(bounds);
     }
 }
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index 1f341147..e0c0c2c 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -22,7 +22,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.window.flags.Flags;
+import com.android.server.wm.utils.DesktopModeFlagsUtil;
 
 /**
  * Constants for desktop mode feature
@@ -35,8 +35,8 @@
             "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
 
     /** Whether desktop mode is enabled. */
-    static boolean isDesktopModeEnabled() {
-        return Flags.enableDesktopWindowingMode();
+    static boolean isDesktopModeEnabled(@NonNull Context context) {
+        return DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE.isEnabled(context);
     }
 
     /**
@@ -60,7 +60,7 @@
      * Return {@code true} if desktop mode can be entered on the current device.
      */
     static boolean canEnterDesktopMode(@NonNull Context context) {
-        return isDesktopModeEnabled()
+        return isDesktopModeEnabled(context)
                 && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context));
     }
 }
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index aacd3c6..548addb 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -25,7 +25,6 @@
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
-import android.os.SystemProperties;
 import android.util.Slog;
 
 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
@@ -38,19 +37,9 @@
             TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
     private static final boolean DEBUG = false;
 
-    public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
-            SystemProperties
-                    .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
-
-    /**
-     * Flag to indicate whether to restrict desktop mode to supported devices.
-     */
-    private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
-            "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
-
     private StringBuilder mLogBuilder;
 
-    private final Context mContext;
+    @NonNull private final Context mContext;
 
     DesktopModeLaunchParamsModifier(@NonNull Context context) {
         mContext = context;
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 3483842..dcadb0f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -397,9 +397,11 @@
                 onRequestedVisibleTypesChanged(newControlTargets.valueAt(i));
             }
             newControlTargets.clear();
-            // Check for and try to run the scheduled show IME request (if it exists), as we
-            // now applied the surface transaction and notified the target of the new control.
-            getImeSourceProvider().checkAndStartShowImePostLayout();
+            if (!android.view.inputmethod.Flags.refactorInsetsController()) {
+                // Check for and try to run the scheduled show IME request (if it exists), as we
+                // now applied the surface transaction and notified the target of the new control.
+                getImeSourceProvider().checkAndStartShowImePostLayout();
+            }
         });
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 60d3e78..12c5073 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY;
 import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.res.Configuration.ASSETS_SEQ_UNDEFINED;
 import static android.os.Build.VERSION_CODES.Q;
@@ -73,6 +74,7 @@
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -112,6 +114,13 @@
     private static final String TAG_RELEASE = TAG + POSTFIX_RELEASE;
     private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION;
 
+    /**
+     * The max number of processes which can be top scheduling group if there are non-top visible
+     * freeform activities run in the process.
+     */
+    private static final int MAX_NUM_PERCEPTIBLE_FREEFORM =
+            SystemProperties.getInt("persist.wm.max_num_perceptible_freeform", 1);
+
     private static final int MAX_RAPID_ACTIVITY_LAUNCH_COUNT = 200;
     private static final long RAPID_ACTIVITY_LAUNCH_MS = 500;
     private static final long RESET_RAPID_ACTIVITY_LAUNCH_MS = 3 * RAPID_ACTIVITY_LAUNCH_MS;
@@ -318,6 +327,7 @@
     public static final int ACTIVITY_STATE_FLAG_HAS_RESUMED = 1 << 21;
     public static final int ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK = 1 << 22;
     public static final int ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN = 1 << 23;
+    public static final int ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM = 1 << 24;
     public static final int ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER = 0x0000ffff;
 
     /**
@@ -1229,6 +1239,7 @@
         ActivityRecord.State bestInvisibleState = DESTROYED;
         boolean allStoppingFinishing = true;
         boolean visible = false;
+        boolean hasResumedFreeform = false;
         int minTaskLayer = Integer.MAX_VALUE;
         int stateFlags = 0;
         final boolean wasResumed = hasResumedActivity();
@@ -1256,6 +1267,8 @@
                                     .processPriorityPolicyForMultiWindowMode()
                             && task.getAdjacentTask() != null) {
                         stateFlags |= ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN;
+                    } else if (windowingMode == WINDOWING_MODE_FREEFORM) {
+                        hasResumedFreeform = true;
                     }
                 }
                 if (minTaskLayer > 0) {
@@ -1289,6 +1302,12 @@
             }
         }
 
+        if (hasResumedFreeform
+                && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode()
+                // Exclude task layer 1 because it is already the top most.
+                && minTaskLayer > 1 && minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) {
+            stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM;
+        }
         stateFlags |= minTaskLayer & ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER;
         if (visible) {
             stateFlags |= ACTIVITY_STATE_FLAG_IS_VISIBLE;
@@ -2105,6 +2124,9 @@
                     if ((stateFlags & ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN) != 0) {
                         pw.print("RS|");
                     }
+                    if ((stateFlags & ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) {
+                        pw.print("PF|");
+                    }
                 }
             } else if ((stateFlags & ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED) != 0) {
                 pw.print("P|");
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 9ebb89d..a36cff6 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -4648,14 +4648,16 @@
         if (!isImeLayeringTarget()) {
             return false;
         }
-        // Note that we don't process IME window if the IME input target is not on the screen.
-        // In case some unexpected IME visibility cases happen like starting the remote
-        // animation on the keyguard but seeing the IME window that originally on the app
-        // which behinds the keyguard.
-        final WindowState imeInputTarget = getImeInputTarget();
-        if (imeInputTarget != null
-                && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) {
-            return false;
+        if (!com.android.window.flags.Flags.doNotSkipImeByTargetVisibility()) {
+            // Note that we don't process IME window if the IME input target is not on the screen.
+            // In case some unexpected IME visibility cases happen like starting the remote
+            // animation on the keyguard but seeing the IME window that originally on the app
+            // which behinds the keyguard.
+            final WindowState imeInputTarget = getImeInputTarget();
+            if (imeInputTarget != null
+                    && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) {
+                return false;
+            }
         }
         return mDisplayContent.forAllImeWindows(callback, traverseTopToBottom);
     }
@@ -5504,7 +5506,8 @@
 
     @Override
     public SurfaceControl getAnimationLeashParent() {
-        if (isStartingWindowAssociatedToTask()) {
+        if (mActivityRecord != null && !mActivityRecord.hasFixedRotationTransform()
+                && isStartingWindowAssociatedToTask()) {
             return mStartingData.mAssociatedTask.mSurfaceControl;
         }
         return super.getAnimationLeashParent();
diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
new file mode 100644
index 0000000..4211764
--- /dev/null
+++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
@@ -0,0 +1,173 @@
+/*
+ * 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.wm.utils;
+
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.window.flags.Flags;
+
+import java.util.function.Supplier;
+
+/**
+ * Util to check desktop mode flags state.
+ *
+ * This utility is used to allow developer option toggles to override flags related to desktop
+ * windowing.
+ *
+ * Computes whether Desktop Windowing related flags should be enabled by using the aconfig flag
+ * value and the developer option override state (if applicable).
+ *
+ * This is a partial copy of {@link com.android.wm.shell.shared.desktopmode.DesktopModeFlags} which
+ * is to be used in WM core.
+ */
+public enum DesktopModeFlagsUtil {
+    // All desktop mode related flags to be overridden by developer option toggle will be added here
+    DESKTOP_WINDOWING_MODE(
+            Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true),
+    WALLPAPER_ACTIVITY(
+            Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true);
+
+    private static final String TAG = "DesktopModeFlagsUtil";
+    private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
+            "sys.wmshell.desktopmode.dev_toggle_override";
+
+    // Function called to obtain aconfig flag value.
+    private final Supplier<Boolean> mFlagFunction;
+    // Whether the flag state should be affected by developer option.
+    private final boolean mShouldOverrideByDevOption;
+
+    // Local cache for toggle override, which is initialized once on its first access. It needs to
+    // be refreshed only on reboots as overridden state takes effect on reboots.
+    private static ToggleOverride sCachedToggleOverride;
+
+    DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) {
+        this.mFlagFunction = flagFunction;
+        this.mShouldOverrideByDevOption = shouldOverrideByDevOption;
+    }
+
+    /**
+     * Determines state of flag based on the actual flag and desktop mode developer option
+     * overrides.
+     *
+     * Note: this method makes sure that a constant developer toggle overrides is read until
+     * reboot.
+     */
+    public boolean isEnabled(Context context) {
+        if (!Flags.showDesktopWindowingDevOption()
+                || !mShouldOverrideByDevOption
+                || context.getContentResolver() == null) {
+            return mFlagFunction.get();
+        } else {
+            boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode();
+            return switch (getToggleOverride(context)) {
+                case OVERRIDE_UNSET -> mFlagFunction.get();
+                // When toggle override matches its default state, don't override flags. This
+                // helps users reset their feature overrides.
+                case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get();
+                case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true;
+            };
+        }
+    }
+
+    private ToggleOverride getToggleOverride(Context context) {
+        // If cached, return it
+        if (sCachedToggleOverride != null) {
+            return sCachedToggleOverride;
+        }
+
+        // Otherwise, fetch and cache it
+        ToggleOverride override = getToggleOverrideFromSystem(context);
+        sCachedToggleOverride = override;
+        Log.d(TAG, "Toggle override initialized to: " + override);
+        return override;
+    }
+
+    /**
+     *  Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise
+     *  initializes the system property by reading Settings.Global.
+     */
+    private ToggleOverride getToggleOverrideFromSystem(Context context) {
+        // A non-persistent System Property is used to store override to ensure it remains
+        // constant till reboot.
+        String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null);
+        ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty);
+
+        // If valid system property, return it
+        if (overrideFromSystemProperties != null) {
+            return overrideFromSystemProperties;
+        }
+
+        // Fallback when System Property is not present (just after reboot) or not valid (user
+        // manually changed the value): Read from Settings.Global
+        int settingValue = Settings.Global.getInt(
+                context.getContentResolver(),
+                Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
+                OVERRIDE_UNSET.getSetting()
+        );
+        ToggleOverride overrideFromSettingsGlobal =
+                ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET);
+        // Initialize System Property
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue));
+        return overrideFromSettingsGlobal;
+    }
+
+    /**
+     * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if
+     * {@code intString} does not correspond to a {@link ToggleOverride}.
+     */
+    private static @Nullable ToggleOverride convertToToggleOverride(
+            @Nullable String intString
+    ) {
+        if (intString == null) return null;
+        try {
+            int intValue = Integer.parseInt(intString);
+            return ToggleOverride.fromSetting(intValue, null);
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "Unknown toggleOverride int " + intString);
+            return null;
+        }
+    }
+
+    /** Override state of desktop mode developer option toggle. */
+    enum ToggleOverride {
+        OVERRIDE_UNSET,
+        OVERRIDE_OFF,
+        OVERRIDE_ON;
+
+        int getSetting() {
+            return switch (this) {
+                case OVERRIDE_ON -> 1;
+                case OVERRIDE_OFF -> 0;
+                case OVERRIDE_UNSET -> -1;
+            };
+        }
+
+        static ToggleOverride fromSetting(int setting, @Nullable ToggleOverride fallback) {
+            return switch (setting) {
+                case 1 -> OVERRIDE_ON;
+                case 0 -> OVERRIDE_OFF;
+                case -1 -> OVERRIDE_UNSET;
+                default -> fallback;
+            };
+        }
+    }
+}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 5719810..4d6a90c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -104,7 +104,6 @@
 
 static struct {
     jclass clazz;
-    jmethodID notifyConfigurationChanged;
     jmethodID notifyInputDevicesChanged;
     jmethodID notifySwitch;
     jmethodID notifyInputChannelBroken;
@@ -314,7 +313,6 @@
 
     void getReaderConfiguration(InputReaderConfiguration* outConfig) override;
     void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override;
-    void notifyConfigurationChanged(nsecs_t when) override;
     std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay(
             const InputDeviceIdentifier& identifier,
             const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) override;
@@ -940,18 +938,6 @@
     checkAndClearExceptionFromCallback(env, "notifySwitch");
 }
 
-void NativeInputManager::notifyConfigurationChanged(nsecs_t when) {
-#if DEBUG_INPUT_DISPATCHER_POLICY
-    ALOGD("notifyConfigurationChanged - when=%lld", when);
-#endif
-    ATRACE_CALL();
-
-    JNIEnv* env = jniEnv();
-
-    env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyConfigurationChanged, when);
-    checkAndClearExceptionFromCallback(env, "notifyConfigurationChanged");
-}
-
 static jobject getInputApplicationHandleObjLocalRef(
         JNIEnv* env, const std::shared_ptr<InputApplicationHandle>& inputApplicationHandle) {
     if (inputApplicationHandle == nullptr) {
@@ -2873,9 +2859,6 @@
     FIND_CLASS(clazz, "com/android/server/input/InputManagerService");
     gServiceClassInfo.clazz = reinterpret_cast<jclass>(env->NewGlobalRef(clazz));
 
-    GET_METHOD_ID(gServiceClassInfo.notifyConfigurationChanged, clazz,
-            "notifyConfigurationChanged", "(J)V");
-
     GET_METHOD_ID(gServiceClassInfo.notifyInputDevicesChanged, clazz,
             "notifyInputDevicesChanged", "([Landroid/view/InputDevice;)V");
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
index dc8cec9..6a0dd5a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
@@ -182,6 +182,7 @@
     private static final String TAG_CREDENTIAL_MANAGER_POLICY = "credential-manager-policy";
     private static final String TAG_DIALER_PACKAGE = "dialer_package";
     private static final String TAG_SMS_PACKAGE = "sms_package";
+    private static final String TAG_PROVISIONING_CONTEXT = "provisioning-context";
 
     // If the ActiveAdmin is a permission-based admin, then info will be null because the
     // permission-based admin is not mapped to a device administrator component.
@@ -359,6 +360,8 @@
     int mWifiMinimumSecurityLevel = DevicePolicyManager.WIFI_SECURITY_OPEN;
     String mDialerPackage;
     String mSmsPackage;
+    private String mProvisioningContext;
+    private static final int PROVISIONING_CONTEXT_LENGTH_LIMIT = 1000;
 
     ActiveAdmin(DeviceAdminInfo info, boolean isParent) {
         this.userId = -1;
@@ -404,6 +407,23 @@
         return UserHandle.of(UserHandle.getUserId(info.getActivityInfo().applicationInfo.uid));
     }
 
+    /**
+     * Stores metadata about context of setting an active admin
+     * @param provisioningContext some metadata, for example test method name
+     */
+    public void setProvisioningContext(@Nullable String provisioningContext) {
+        if (Flags.provisioningContextParameter()
+                && !TextUtils.isEmpty(provisioningContext)
+                && !provisioningContext.isBlank()) {
+            if (provisioningContext.length() > PROVISIONING_CONTEXT_LENGTH_LIMIT) {
+                mProvisioningContext = provisioningContext.substring(
+                        0, PROVISIONING_CONTEXT_LENGTH_LIMIT);
+            } else {
+                mProvisioningContext = provisioningContext;
+            }
+        }
+    }
+
     void writeToXml(TypedXmlSerializer out)
             throws IllegalArgumentException, IllegalStateException, IOException {
         if (info != null) {
@@ -694,6 +714,12 @@
         if (!TextUtils.isEmpty(mSmsPackage)) {
             writeAttributeValueToXml(out, TAG_SMS_PACKAGE, mSmsPackage);
         }
+
+        if (Flags.provisioningContextParameter() && !TextUtils.isEmpty(mProvisioningContext)) {
+            out.startTag(null, TAG_PROVISIONING_CONTEXT);
+            out.attribute(null, ATTR_VALUE, mProvisioningContext);
+            out.endTag(null, TAG_PROVISIONING_CONTEXT);
+        }
     }
 
     private void writePackagePolicy(TypedXmlSerializer out, String tag,
@@ -1006,6 +1032,9 @@
                 mDialerPackage = parser.getAttributeValue(null, ATTR_VALUE);
             } else if (TAG_SMS_PACKAGE.equals(tag)) {
                 mSmsPackage = parser.getAttributeValue(null, ATTR_VALUE);
+            } else if (Flags.provisioningContextParameter()
+                    && TAG_PROVISIONING_CONTEXT.equals(tag)) {
+                mProvisioningContext = parser.getAttributeValue(null, ATTR_VALUE);
             } else {
                 Slogf.w(LOG_TAG, "Unknown admin tag: %s", tag);
                 XmlUtils.skipCurrentTag(parser);
@@ -1496,5 +1525,10 @@
         pw.println(mDialerPackage);
         pw.print("mSmsPackage=");
         pw.println(mSmsPackage);
+
+        if (Flags.provisioningContextParameter()) {
+            pw.print("mProvisioningContext=");
+            pw.println(mProvisioningContext);
+        }
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 032d6b5..8cc7383 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -3943,10 +3943,16 @@
     /**
      * @param adminReceiver The admin to add
      * @param refreshing true = update an active admin, no error
+     * @param userHandle which user this admin will be set on
+     * @param provisioningContext additional information for debugging
      */
     @Override
     public void setActiveAdmin(
-            ComponentName adminReceiver, boolean refreshing, int userHandle) {
+            ComponentName adminReceiver,
+            boolean refreshing,
+            int userHandle,
+            @Nullable String provisioningContext
+    ) {
         if (!mHasFeature) {
             return;
         }
@@ -3972,6 +3978,7 @@
                 newAdmin.testOnlyAdmin =
                         (existingAdmin != null) ? existingAdmin.testOnlyAdmin
                                 : isPackageTestOnly(adminReceiver.getPackageName(), userHandle);
+                newAdmin.setProvisioningContext(provisioningContext);
                 policy.mAdminMap.put(adminReceiver, newAdmin);
                 int replaceIndex = -1;
                 final int N = policy.mAdminList.size();
@@ -12830,7 +12837,7 @@
         });
 
         // Set admin.
-        setActiveAdmin(profileOwner, /* refreshing= */ true, userId);
+        setActiveAdmin(profileOwner, /* refreshing= */ true, userId, null);
         setProfileOwner(profileOwner, userId);
 
         synchronized (getLockObject()) {
@@ -21883,7 +21890,7 @@
             @UserIdInt int userId, @UserIdInt int callingUserId, ComponentName adminComponent) {
         final String adminPackage = adminComponent.getPackageName();
         enablePackage(adminPackage, callingUserId);
-        setActiveAdmin(adminComponent, /* refreshing= */ true, userId);
+        setActiveAdmin(adminComponent, /* refreshing= */ true, userId, null);
     }
 
     private void enablePackage(String packageName, @UserIdInt int userId) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
index eb893fc..0cd5b47 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
@@ -17,6 +17,7 @@
 
 import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
 import android.content.ComponentName;
 import android.os.ShellCommand;
 import android.os.SystemClock;
@@ -46,11 +47,13 @@
 
     private static final String USER_OPTION = "--user";
     private static final String DO_ONLY_OPTION = "--device-owner-only";
+    private static final String PROVISIONING_CONTEXT_OPTION = "--provisioning-context";
 
     private final DevicePolicyManagerService mService;
     private int mUserId = UserHandle.USER_SYSTEM;
     private ComponentName mComponent;
     private boolean mSetDoOnly;
+    private String mProvisioningContext = null;
 
     DevicePolicyManagerServiceShellCommand(DevicePolicyManagerService service) {
         mService = Objects.requireNonNull(service);
@@ -127,15 +130,28 @@
         pw.printf("    Lists the device / profile owners per user \n\n");
         pw.printf("  %s\n", CMD_LIST_POLICY_EXEMPT_APPS);
         pw.printf("    Lists the apps that are exempt from policies\n\n");
-        pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
-                CMD_SET_ACTIVE_ADMIN, USER_OPTION);
-        pw.printf("    Sets the given component as active admin for an existing user.\n\n");
-        pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]"
-                + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION);
-        pw.printf("    Sets the given component as active admin, and its package as device owner."
-                + "\n\n");
-        pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
-                CMD_SET_PROFILE_OWNER, USER_OPTION);
+        if (Flags.provisioningContextParameter()) {
+            pw.printf("  %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n",
+                    CMD_SET_ACTIVE_ADMIN, USER_OPTION, PROVISIONING_CONTEXT_OPTION);
+            pw.printf("    Sets the given component as active admin for an existing user.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]"
+                            + " [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n",
+                    CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION, PROVISIONING_CONTEXT_OPTION);
+            pw.printf("    Sets the given component as active admin, and its package as device"
+                    + " owner.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n",
+                    CMD_SET_PROFILE_OWNER, USER_OPTION, PROVISIONING_CONTEXT_OPTION);
+        } else {
+            pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
+                    CMD_SET_ACTIVE_ADMIN, USER_OPTION);
+            pw.printf("    Sets the given component as active admin for an existing user.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]"
+                    + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION);
+            pw.printf("    Sets the given component as active admin, and its package as device"
+                    + " owner.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
+                    CMD_SET_PROFILE_OWNER, USER_OPTION);
+        }
         pw.printf("    Sets the given component as active admin and profile owner for an existing "
                 + "user.\n\n");
         pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
@@ -243,7 +259,7 @@
 
     private int runSetActiveAdmin(PrintWriter pw) {
         parseArgs();
-        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId);
+        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext);
 
         pw.printf("Success: Active admin set to component %s\n", mComponent.flattenToShortString());
         return 0;
@@ -253,7 +269,12 @@
         parseArgs();
         boolean isAdminAdded = false;
         try {
-            mService.setActiveAdmin(mComponent, /* refreshing= */ false, mUserId);
+            mService.setActiveAdmin(
+                    mComponent,
+                    /* refreshing= */ false,
+                    mUserId,
+                    mProvisioningContext
+            );
             isAdminAdded = true;
         } catch (IllegalArgumentException e) {
             pw.printf("%s was already an admin for user %d. No need to set it again.\n",
@@ -291,7 +312,7 @@
 
     private int runSetProfileOwner(PrintWriter pw) {
         parseArgs();
-        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId);
+        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext);
 
         try {
             if (!mService.setProfileOwner(mComponent, mUserId)) {
@@ -363,6 +384,8 @@
                 }
             } else if (DO_ONLY_OPTION.equals(opt)) {
                 mSetDoOnly = true;
+            } else if (PROVISIONING_CONTEXT_OPTION.equals(opt)) {
+                mProvisioningContext = getNextArgRequired();
             } else {
                 throw new IllegalArgumentException("Unknown option: " + opt);
             }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
index e982153..e04716e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -41,8 +42,8 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.display.DisplayBrightnessState;
-import com.android.server.display.DisplayDeviceConfig;
 import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState;
 import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DeviceConfigParameterProvider;
 import com.android.server.display.feature.DisplayManagerFlags;
@@ -89,6 +90,10 @@
     @Mock
     private BrightnessModifier mMockModifier;
     @Mock
+    private TestStatefulModifier mMockStatefulModifier;
+    @Mock
+    private TestDisplayListenerModifier mMockDisplayListenerModifier;
+    @Mock
     private DisplayManagerInternal.DisplayPowerRequest mMockRequest;
 
     @Mock
@@ -99,7 +104,8 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mTestInjector = new TestInjector(List.of(mMockClamper), List.of(mMockModifier));
+        mTestInjector = new TestInjector(List.of(mMockClamper),
+                List.of(mMockModifier, mMockStatefulModifier, mMockDisplayListenerModifier));
         when(mMockDisplayDeviceData.getDisplayId()).thenReturn(DISPLAY_ID);
         when(mMockDisplayDeviceData.getAmbientLightSensor()).thenReturn(mMockSensorData);
 
@@ -168,6 +174,13 @@
     }
 
     @Test
+    public void testOnDisplayChanged_DelegatesToDisplayListeners() {
+        mClamperController.onDisplayChanged(mMockDisplayDeviceData);
+
+        verify(mMockDisplayListenerModifier).onDisplayChanged(mMockDisplayDeviceData);
+    }
+
+    @Test
     public void testOnDisplayChanged_doesNotRestartLightSensor() {
         mClamperController.onDisplayChanged(mMockDisplayDeviceData);
 
@@ -189,6 +202,8 @@
         mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_ON);
 
         verify(mMockModifier).apply(eq(mMockRequest), any());
+        verify(mMockDisplayListenerModifier).apply(eq(mMockRequest), any());
+        verify(mMockStatefulModifier).apply(eq(mMockRequest), any());
     }
 
     @Test
@@ -326,11 +341,40 @@
         verify(mMockClamper).stop();
     }
 
+    @Test
+    public void test_doesNotNotifyExternalListener_aggregatedStateNotChanged() {
+        mTestInjector.mCapturedChangeListener.onChanged();
+        mTestHandler.flush();
+
+        verify(mMockExternalListener, never()).onChanged();
+    }
+
+    @Test
+    public void test_notifiesExternalListener_aggregatedStateChanged() {
+        doAnswer((invocation) -> {
+            ModifiersAggregatedState argument = invocation.getArgument(0);
+            argument.mHdrHbmEnabled = true;
+            return null;
+        }).when(mMockStatefulModifier).applyStateChange(any());
+        mTestInjector.mCapturedChangeListener.onChanged();
+        mTestHandler.flush();
+
+        verify(mMockExternalListener).onChanged();
+    }
+
     private BrightnessClamperController createBrightnessClamperController() {
         return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener,
                 mMockDisplayDeviceData, mMockContext, mFlags, mSensorManager);
     }
 
+    interface TestDisplayListenerModifier extends BrightnessStateModifier,
+            BrightnessClamperController.DisplayDeviceDataListener {
+    }
+
+    interface TestStatefulModifier extends BrightnessStateModifier,
+            BrightnessClamperController.StatefulModifier {
+    }
+
     private class TestInjector extends BrightnessClamperController.Injector {
 
         private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>>
@@ -366,7 +410,7 @@
         @Override
         List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
                 Handler handler, BrightnessClamperController.ClamperChangeListener listener,
-                DisplayDeviceConfig displayDeviceConfig) {
+                BrightnessClamperController.DisplayDeviceData displayDeviceData) {
             return mModifiers;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt
new file mode 100644
index 0000000..5fd848f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.display.brightness.clamper
+
+import android.os.IBinder
+import android.view.Display
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.brightness.clamper.BrightnessClamperController.DisplayDeviceData
+
+fun createDisplayDeviceData(
+    displayDeviceConfig: DisplayDeviceConfig,
+    displayToken: IBinder,
+    uniqueDisplayId: String = "displayId",
+    thermalThrottlingDataId: String = "thermalId",
+    powerThrottlingDataId: String = "powerId",
+    width: Int = 100,
+    height: Int = 100,
+    displayId: Int = Display.DEFAULT_DISPLAY
+): DisplayDeviceData {
+    return DisplayDeviceData(
+        uniqueDisplayId,
+        thermalThrottlingDataId,
+        powerThrottlingDataId,
+        displayDeviceConfig,
+        width,
+        height,
+        displayToken,
+        displayId
+    )
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
new file mode 100644
index 0000000..e9ec811
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.display.brightness.clamper
+
+import android.hardware.display.DisplayManagerInternal
+import android.os.IBinder
+import android.util.Spline
+import android.view.SurfaceControlHdrLayerInfoListener
+import androidx.test.filters.SmallTest
+import com.android.server.display.DisplayBrightnessState
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ClamperChangeListener
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState
+import com.android.server.display.brightness.clamper.HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO
+import com.android.server.display.brightness.clamper.HdrBrightnessModifier.Injector
+import com.android.server.display.config.createHdrBrightnessData
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+class HdrBrightnessModifierTest {
+
+    private val testHandler = TestHandler(null)
+    private val testInjector = TestInjector()
+    private val mockChangeListener = mock<ClamperChangeListener>()
+    private val mockDisplayDeviceConfig = mock<DisplayDeviceConfig>()
+    private val mockDisplayBinder = mock<IBinder>()
+    private val mockDisplayBinderOther = mock<IBinder>()
+    private val mockSpline = mock<Spline>()
+    private val mockRequest = mock<DisplayManagerInternal.DisplayPowerRequest>()
+
+    private lateinit var modifier: HdrBrightnessModifier
+    private val dummyData = createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinder)
+    private val dummyHdrData = createHdrBrightnessData()
+
+    @Test
+    fun `change listener is not called on init`() {
+        initHdrModifier()
+
+        verify(mockChangeListener, never()).onChanged()
+    }
+
+    @Test
+    fun `hdr listener registered on init if hdr data is present`() {
+        initHdrModifier()
+
+        assertThat(testInjector.registeredHdrListener).isNotNull()
+        assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinder)
+    }
+
+    @Test
+    fun `hdr listener not registered on init if hdr data is missing`() {
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null)
+        modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData)
+
+        testHandler.flush()
+
+        assertThat(testInjector.registeredHdrListener).isNull()
+        assertThat(testInjector.registeredToken).isNull()
+    }
+
+    @Test
+    fun `unsubscribes hdr listener when display changed with no hdr data`() {
+        initHdrModifier()
+
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null)
+        modifier.onDisplayChanged(dummyData)
+        testHandler.flush()
+
+        assertThat(testInjector.registeredHdrListener).isNull()
+        assertThat(testInjector.registeredToken).isNull()
+        verify(mockChangeListener, never()).onChanged()
+    }
+
+    @Test
+    fun `resubscribes hdr listener when display changed with different token`() {
+        initHdrModifier()
+
+        modifier.onDisplayChanged(
+            createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinderOther))
+        testHandler.flush()
+
+        assertThat(testInjector.registeredHdrListener).isNotNull()
+        assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinderOther)
+        verify(mockChangeListener, never()).onChanged()
+    }
+
+    @Test
+    fun `test NO_HDR mode`() {
+        initHdrModifier()
+
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData(
+            minimumHdrPercentOfScreenForNbm = 0.5f,
+            minimumHdrPercentOfScreenForHbm = 0.7f,
+            sdrToHdrRatioSpline = mockSpline
+        ))
+        // screen size = 10_000
+        modifier.onDisplayChanged(createDisplayDeviceData(
+            mockDisplayDeviceConfig, mockDisplayBinder,
+            width = 100,
+            height = 100
+        ))
+        testHandler.flush()
+        // hdr size = 900
+        val desiredMaxHdrRatio = 8f
+        val hdrWidth = 30
+        val hdrHeight = 30
+        testInjector.registeredHdrListener!!.onHdrInfoChanged(
+            mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio
+        )
+        testHandler.flush()
+
+        val modifierState = ModifiersAggregatedState()
+        modifier.applyStateChange(modifierState)
+
+        assertThat(modifierState.mHdrHbmEnabled).isFalse()
+        assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(DEFAULT_MAX_HDR_SDR_RATIO)
+        assertThat(modifierState.mSdrHdrRatioSpline).isNull()
+
+        val stateBuilder = DisplayBrightnessState.builder()
+        modifier.apply(mockRequest, stateBuilder)
+
+        verify(mockDisplayDeviceConfig, never()).getHdrBrightnessFromSdr(any(), any(), any())
+        assertThat(stateBuilder.hdrBrightness).isEqualTo(DisplayBrightnessState.BRIGHTNESS_NOT_SET)
+    }
+
+    @Test
+    fun `test NBM_HDR mode`() {
+        initHdrModifier()
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData(
+            minimumHdrPercentOfScreenForNbm = 0.5f,
+            minimumHdrPercentOfScreenForHbm = 0.7f,
+            sdrToHdrRatioSpline = mockSpline
+        ))
+        // screen size = 10_000
+        modifier.onDisplayChanged(createDisplayDeviceData(
+            mockDisplayDeviceConfig, mockDisplayBinder,
+            width = 100,
+            height = 100
+        ))
+        testHandler.flush()
+        // hdr size = 5_100
+        val desiredMaxHdrRatio = 8f
+        val hdrWidth = 100
+        val hdrHeight = 51
+        testInjector.registeredHdrListener!!.onHdrInfoChanged(
+            mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio
+        )
+        testHandler.flush()
+
+        val modifierState = ModifiersAggregatedState()
+        modifier.applyStateChange(modifierState)
+
+        assertThat(modifierState.mHdrHbmEnabled).isFalse()
+        assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(desiredMaxHdrRatio)
+        assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(mockSpline)
+
+        val expectedHdrBrightness = 0.85f
+        whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+            0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness)
+        val stateBuilder = DisplayBrightnessState.builder()
+        modifier.apply(mockRequest, stateBuilder)
+
+        assertThat(stateBuilder.hdrBrightness).isEqualTo(expectedHdrBrightness)
+    }
+
+    @Test
+    fun `test HBM_HDR mode`() {
+        initHdrModifier()
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData(
+            minimumHdrPercentOfScreenForNbm = 0.5f,
+            minimumHdrPercentOfScreenForHbm = 0.7f,
+            sdrToHdrRatioSpline = mockSpline
+        ))
+        // screen size = 10_000
+        modifier.onDisplayChanged(createDisplayDeviceData(
+            mockDisplayDeviceConfig, mockDisplayBinder,
+            width = 100,
+            height = 100
+        ))
+        testHandler.flush()
+        // hdr size = 7_100
+        val desiredMaxHdrRatio = 8f
+        val hdrWidth = 100
+        val hdrHeight = 71
+        testInjector.registeredHdrListener!!.onHdrInfoChanged(
+            mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio
+        )
+        testHandler.flush()
+
+        val modifierState = ModifiersAggregatedState()
+        modifier.applyStateChange(modifierState)
+
+        assertThat(modifierState.mHdrHbmEnabled).isTrue()
+        assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(desiredMaxHdrRatio)
+        assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(mockSpline)
+
+        val expectedHdrBrightness = 0.83f
+        whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+            0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness)
+        val stateBuilder = DisplayBrightnessState.builder()
+        modifier.apply(mockRequest, stateBuilder)
+
+        assertThat(stateBuilder.hdrBrightness).isEqualTo(expectedHdrBrightness)
+    }
+
+    private fun initHdrModifier() {
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(dummyHdrData)
+        modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData)
+        testHandler.flush()
+    }
+
+
+    internal class TestInjector : Injector() {
+        var registeredHdrListener: SurfaceControlHdrLayerInfoListener? = null
+        var registeredToken: IBinder? = null
+
+        override fun registerHdrListener(
+            listener: SurfaceControlHdrLayerInfoListener, token: IBinder
+        ) {
+            registeredHdrListener = listener
+            registeredToken = token
+        }
+
+        override fun unregisterHdrListener(
+            listener: SurfaceControlHdrLayerInfoListener, token: IBinder
+        ) {
+            registeredHdrListener = null
+            registeredToken = null
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
index 72883e2..5bd919f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
@@ -23,6 +23,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertFalse;
 
 import android.os.Environment;
 import android.os.FileUtils;
@@ -51,6 +52,7 @@
     private static final int USER_ID_1 = 10;
     private static final int USER_ID_2 = 11;
     private static final int USER_ID_3 = 12;
+    private static final int USER_ID_SYSTEM = 0;
     private static final long TEST_TIMESTAMP = 150_000;
     private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry
             .getInstrumentation().getContext().getDataDir(), "alarmsTestDir");
@@ -110,6 +112,14 @@
     }
 
     @Test
+    public void testAddWakeupForSystemUser_shouldDoNothing() {
+        mUserWakeupStore.addUserWakeup(USER_ID_SYSTEM, TEST_TIMESTAMP - 19_000);
+        assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml");
+        assertFalse(file.exists());
+    }
+
+    @Test
     public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() {
         final long finalAlarmTime = TEST_TIMESTAMP - 13_000;
         mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 1dbd532..8656b99 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -500,6 +500,13 @@
         updateOomAdj(app);
         assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
         assertEquals("resumed-split-screen-activity", app.mState.getAdjType());
+
+        doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE
+                | WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM)
+                .when(wpc).getActivityStateFlags();
+        updateOomAdj(app);
+        assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
+        assertEquals("perceptible-freeform-activity", app.mState.getAdjType());
     }
 
     @SuppressWarnings("GuardedBy")
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index b2a5b02..f2b4136 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -59,7 +59,6 @@
     name: "PowerStatsTestsRavenwood",
     static_libs: [
         "services.core",
-        "platformprotosnano",
         "coretests-aidl",
         "ravenwood-junit",
         "truth",
diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index 758c84a..ef9580c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -101,7 +101,7 @@
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class)) {
+                mock(AudioServerPermissionProvider.class), r -> r.run()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 2cb02bd..4645156 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -78,7 +78,7 @@
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class)) {
+                mock(AudioServerPermissionProvider.class), r -> r.run()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index 037c3c0..b7100ea 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -87,7 +87,7 @@
                 .thenReturn(AppOpsManager.MODE_ALLOWED);
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null,
-                mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider);
+                mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider, r -> r.run());
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index 27b552f..746645a 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -78,7 +78,7 @@
         mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class));
+                mock(AudioServerPermissionProvider.class), r -> r.run());
         mTestLooper.dispatchAll();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index 8e34ee1..e45ab31 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -160,7 +160,7 @@
                 @NonNull PermissionEnforcer enforcer,
                 AudioServerPermissionProvider permissionProvider) {
             super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper,
-                    audioPolicy, looper, appOps, enforcer, permissionProvider);
+                    audioPolicy, looper, appOps, enforcer, permissionProvider, r -> r.run());
         }
 
         public void setDeviceForStream(int stream, int device) {
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
index 8a9538f..ebdde94 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
@@ -66,7 +66,7 @@
                 any(UserHandle.class));
         mDpmTestable = new DevicePolicyManagerServiceTestable(getServices(), mSpiedDpmMockContext);
         setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_UID);
-        mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE);
+        mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE, null);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index 54d1138..cbf7935 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -16,7 +16,6 @@
 
 package com.android.server.webkit;
 
-import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -66,10 +65,12 @@
     }
 
     @Override
-    public String getUserChosenWebViewProvider(Context context) { return mUserProvider; }
+    public String getUserChosenWebViewProvider() {
+        return mUserProvider;
+    }
 
     @Override
-    public void updateUserSetting(Context context, String newProviderName) {
+    public void updateUserSetting(String newProviderName) {
         mUserProvider = newProviderName;
     }
 
@@ -77,14 +78,14 @@
     public void killPackageDependents(String packageName) {}
 
     @Override
-    public void enablePackageForAllUsers(Context context, String packageName, boolean enable) {
+    public void enablePackageForAllUsers(String packageName, boolean enable) {
         for(int userId : mUsers) {
             enablePackageForUser(packageName, enable, userId);
         }
     }
 
     @Override
-    public void installExistingPackageForAllUsers(Context context, String packageName) {
+    public void installExistingPackageForAllUsers(String packageName) {
         for (int userId : mUsers) {
             installPackageForUser(packageName, userId);
         }
@@ -131,8 +132,7 @@
     }
 
     @Override
-    public List<UserPackage> getPackageInfoForProviderAllUsers(
-            Context context, WebViewProviderInfo info) {
+    public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo info) {
         Map<Integer, PackageInfo> userPackages = mPackages.get(info.packageName);
         List<UserPackage> ret = new ArrayList();
         // Loop over defined users, and find the corresponding package for each user.
@@ -185,12 +185,12 @@
     }
 
     @Override
-    public int getMultiProcessSetting(Context context) {
+    public int getMultiProcessSetting() {
         return mMultiProcessSetting;
     }
 
     @Override
-    public void setMultiProcessSetting(Context context, int value) {
+    public void setMultiProcessSetting(int value) {
         mMultiProcessSetting = value;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index e181a51..06479c8 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -104,10 +104,10 @@
         mTestSystemImpl = Mockito.spy(testing);
         if (updateServiceV2()) {
             mWebViewUpdateServiceImpl =
-                    new WebViewUpdateServiceImpl2(null /*Context*/, mTestSystemImpl);
+                    new WebViewUpdateServiceImpl2(mTestSystemImpl);
         } else {
             mWebViewUpdateServiceImpl =
-                    new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl);
+                    new WebViewUpdateServiceImpl(mTestSystemImpl);
         }
     }
 
@@ -140,7 +140,7 @@
             WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) {
         setupWithPackagesAndRelroCount(webviewPackages, numRelros);
         if (userSetting != null) {
-            mTestSystemImpl.updateUserSetting(null, userSetting);
+            mTestSystemImpl.updateUserSetting(userSetting);
         }
         // Add (enabled and valid) package infos for each provider
         setEnabledAndValidPackageInfos(webviewPackages);
@@ -313,7 +313,7 @@
         };
         setupWithPackagesNonDebuggable(packages);
         // Start with the setting pointing to the invalid package
-        mTestSystemImpl.updateUserSetting(null, invalidPackage);
+        mTestSystemImpl.updateUserSetting(invalidPackage);
         mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */,
                     true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature}
                     , 0 /* updateTime */));
@@ -481,7 +481,7 @@
             new WebViewProviderInfo(secondPackage, "", true, false, null)};
         setupWithPackages(packages);
         // Start with the setting pointing to the second package
-        mTestSystemImpl.updateUserSetting(null, secondPackage);
+        mTestSystemImpl.updateUserSetting(secondPackage);
         // Have all packages be enabled, so that we can change provider however we want to
         setEnabledAndValidPackageInfos(packages);
 
@@ -572,7 +572,7 @@
         // Check that the boot time logic re-enables the fallback package.
         runWebViewBootPreparationOnMainSync();
         Mockito.verify(mTestSystemImpl).enablePackageForAllUsers(
-                Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
+                Mockito.eq(testPackage), Mockito.eq(true));
 
         // Fake the message about the enabling having changed the package state,
         // and check we now use that package.
@@ -657,7 +657,7 @@
                     null)};
         setupWithPackages(packages);
         // Start with the setting pointing to the secondary package
-        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
+        mTestSystemImpl.updateUserSetting(secondaryPackage);
         int secondaryUserId = 10;
         int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID;
         if (multiUser) {
@@ -710,7 +710,7 @@
                     null)};
         setupWithPackages(packages);
         // Start with the setting pointing to the secondary package
-        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
+        mTestSystemImpl.updateUserSetting(secondaryPackage);
         setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
         int newUser = 100;
         mTestSystemImpl.addUser(newUser);
@@ -832,14 +832,13 @@
                         true /* installed */));
 
         // Set user-chosen package
-        mTestSystemImpl.updateUserSetting(null, chosenPackage);
+        mTestSystemImpl.updateUserSetting(chosenPackage);
 
         runWebViewBootPreparationOnMainSync();
 
         // Verify that we switch the setting to point to the current package
-        Mockito.verify(mTestSystemImpl).updateUserSetting(
-                Mockito.anyObject(), Mockito.eq(nonChosenPackage));
-        assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider(null));
+        Mockito.verify(mTestSystemImpl).updateUserSetting(Mockito.eq(nonChosenPackage));
+        assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider());
 
         checkPreparationPhasesForPackage(nonChosenPackage, 1);
     }
@@ -976,7 +975,7 @@
         setEnabledAndValidPackageInfos(packages);
 
         // Start with the setting pointing to the third package
-        mTestSystemImpl.updateUserSetting(null, thirdPackage);
+        mTestSystemImpl.updateUserSetting(thirdPackage);
 
         runWebViewBootPreparationOnMainSync();
         checkPreparationPhasesForPackage(thirdPackage, 1);
@@ -1167,7 +1166,7 @@
 
         setupWithPackages(webviewPackages);
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
         int secondaryUserId = 5;
         if (multiUser) {
             mTestSystemImpl.addUser(secondaryUserId);
@@ -1220,7 +1219,7 @@
 
         setupWithPackages(webviewPackages);
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
         int secondaryUserId = 412;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1277,7 +1276,7 @@
 
         setupWithPackages(webviewPackages);
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
         int secondaryUserId = 4;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1290,7 +1289,7 @@
                 0 /* updateTime */, (testHidden ? true : false) /* hidden */));
 
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
 
         runWebViewBootPreparationOnMainSync();
 
@@ -1458,7 +1457,7 @@
         runWebViewBootPreparationOnMainSync();
         checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */);
 
-        mTestSystemImpl.setMultiProcessSetting(null /* context */, settingValue);
+        mTestSystemImpl.setMultiProcessSetting(settingValue);
 
         assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled());
     }
@@ -1492,7 +1491,7 @@
                 };
         setupWithPackages(packages);
         // Start with the setting pointing to the invalid package
-        mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName);
+        mTestSystemImpl.updateUserSetting(oldSdkPackage.packageName);
 
         mTestSystemImpl.setPackageInfo(newSdkPackage);
         mTestSystemImpl.setPackageInfo(currentSdkPackage);
@@ -1545,8 +1544,7 @@
         // Check that the boot time logic re-enables the default package.
         runWebViewBootPreparationOnMainSync();
         Mockito.verify(mTestSystemImpl)
-                .enablePackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
+                .enablePackageForAllUsers(Mockito.eq(testPackage), Mockito.eq(true));
     }
 
     @Test
@@ -1570,8 +1568,7 @@
         // Check that the boot time logic tries to install the default package.
         runWebViewBootPreparationOnMainSync();
         Mockito.verify(mTestSystemImpl)
-                .installExistingPackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage));
+                .installExistingPackageForAllUsers(Mockito.eq(testPackage));
     }
 
     @Test
@@ -1598,8 +1595,7 @@
 
         // Check that we try to re-install the default package.
         Mockito.verify(mTestSystemImpl)
-                .installExistingPackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage));
+                .installExistingPackageForAllUsers(Mockito.eq(testPackage));
     }
 
     /**
@@ -1632,8 +1628,7 @@
 
         // Check that we try to re-install the default package for all users.
         Mockito.verify(mTestSystemImpl)
-                .installExistingPackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage));
+                .installExistingPackageForAllUsers(Mockito.eq(testPackage));
     }
 
     private void testDefaultPackageChosen(PackageInfo packageInfo) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index b4505fa..24fc7ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2955,7 +2955,8 @@
 
     @Test
     public void testStartingWindowInTaskFragment() {
-        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setVisible(false).build();
         final WindowState startingWindow = createWindowState(
                 new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1);
         activity1.addWindow(startingWindow);
@@ -3011,6 +3012,28 @@
     }
 
     @Test
+    public void testStartingWindowInTaskFragmentWithVisibleTask() {
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Task task = activity1.getTask();
+        final Rect taskBounds = task.getBounds();
+        final Rect tfBounds = new Rect(taskBounds.left, taskBounds.top,
+                taskBounds.left + taskBounds.width() / 2, taskBounds.bottom);
+        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm).setParentTask(task)
+                .setBounds(tfBounds).build();
+
+        final ActivityRecord activity2 = new ActivityBuilder(mAtm).build();
+        final WindowState startingWindow = createWindowState(
+                new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1);
+        taskFragment.addChild(activity2);
+        activity2.addWindow(startingWindow);
+        activity2.mStartingData = mock(StartingData.class);
+        activity2.attachStartingWindow(startingWindow);
+
+        assertNull(activity2.mStartingData.mAssociatedTask);
+        assertNull(task.mSharedStartingData);
+    }
+
+    @Test
     public void testTransitionAnimationBounds() {
         removeGlobalMinSizeRestriction();
         final Task task = new TaskBuilder(mSupervisor)
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index 23a88a1..b687042 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -21,9 +21,19 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING;
+import static com.android.server.wm.DesktopModeBoundsCalculator.calculateAspectRatio;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
@@ -59,6 +69,10 @@
 @RunWith(WindowTestRunner.class)
 public class DesktopModeLaunchParamsModifierTests extends
         LaunchParamsModifierTestsBase<DesktopModeLaunchParamsModifier> {
+    private static final Rect LANDSCAPE_DISPLAY_BOUNDS = new Rect(0, 0, 2560, 1600);
+    private static final Rect PORTRAIT_DISPLAY_BOUNDS = new Rect(0, 0, 1600, 2560);
+    private static final float LETTERBOX_ASPECT_RATIO = 1.3f;
+
     @Before
     public void setUp() throws Exception {
         mActivity = new ActivityBuilder(mAtm).build();
@@ -158,6 +172,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
     public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -169,6 +184,209 @@
                 (int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
         final int desiredHeight =
                 (int) (DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_resizable_undefinedOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_resizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testResizablePortraitBounds_landscapeDevice_resizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+        final int desiredWidth =
+                (int) ((LANDSCAPE_DISPLAY_BOUNDS.height() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_unResizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_resizable_undefinedOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_resizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testResizableLandscapeBounds_portraitDevice_resizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+        final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+                - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+        final int desiredHeight = (int)
+                ((PORTRAIT_DISPLAY_BOUNDS.width() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_unResizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+        final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+                - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+        final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO);
+
         assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
         assertEquals(desiredWidth, mResult.mBounds.width());
         assertEquals(desiredHeight, mResult.mBounds.height());
@@ -192,6 +410,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_CenterToDisplay() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -207,6 +426,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_LeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -222,6 +442,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_TopGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -237,6 +458,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_TopLeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -252,6 +474,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_RightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -267,6 +490,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_BottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -282,6 +506,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_RightBottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -297,6 +522,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutFractionBounds() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -312,6 +538,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -327,6 +554,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -342,6 +570,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -359,6 +588,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -374,6 +604,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -389,6 +620,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -422,6 +654,38 @@
         assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
     }
 
+    private Task createTask(DisplayContent display, int orientation, Boolean isResizeable) {
+        final int resizeMode = isResizeable ? RESIZE_MODE_RESIZEABLE
+                : RESIZE_MODE_UNRESIZEABLE;
+        final Task task = new TaskBuilder(mSupervisor).setActivityType(
+                ACTIVITY_TYPE_STANDARD).setDisplay(display).build();
+        task.setResizeMode(resizeMode);
+        mActivity = new ActivityBuilder(task.mAtmService)
+                .setTask(task)
+                .setScreenOrientation(orientation)
+                .setOnTop(true).build();
+
+        mActivity.onDisplayChanged(display);
+        mActivity.setOccludesParent(true);
+        mActivity.setVisible(true);
+        mActivity.setVisibleRequested(true);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(/* ignoreOrientationRequest */ true);
+
+        return task;
+    }
+
+    private TestDisplayContent createDisplayContent(int orientation, Rect displayBounds) {
+        final TestDisplayContent display = new TestDisplayContent
+                .Builder(mAtm, displayBounds.width(), displayBounds.height())
+                .setPosition(DisplayContent.POSITION_TOP).build();
+        display.setBounds(displayBounds);
+        display.getConfiguration().densityDpi = DENSITY_DEFAULT;
+        display.getConfiguration().orientation = ORIENTATION_LANDSCAPE;
+        display.getDefaultTaskDisplayArea().setWindowingMode(orientation);
+
+        return display;
+    }
+
     private void setupDesktopModeLaunchParamsModifier() {
         setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ true,
                 /*enforceDeviceRestrictions=*/ true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index 0bf27d1..f93ffb8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -747,6 +747,8 @@
         }
     }
 
+    @android.platform.test.annotations.RequiresFlagsDisabled(
+            com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY)
     @SetupWindows(addWindows = W_INPUT_METHOD)
     @Test
     public void testLaunchRemoteAnimationWithoutImeBehind() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index a816aa9..d5d2847 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -203,6 +203,7 @@
                 .mockStatic(LockGuard.class, mockStubOnly)
                 .mockStatic(Watchdog.class, mockStubOnly)
                 .spyStatic(DesktopModeHelper.class)
+                .spyStatic(DesktopModeBoundsCalculator.class)
                 .strictness(Strictness.LENIENT)
                 .startMocking();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
index 593e983..22def51 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -23,6 +23,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.eq;
 
 import android.platform.test.annotations.Presubmit;
@@ -60,4 +61,24 @@
         verify(c).accept(eq(mDockedDividerWindow));
         verify(c).accept(eq(mImeWindow));
     }
+
+    @android.platform.test.annotations.RequiresFlagsEnabled(
+            com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY)
+    @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
+    @Test
+    public void testTraverseImeRegardlessOfImeTarget() {
+        mDisplayContent.setImeLayeringTarget(mAppWindow);
+        mDisplayContent.setImeInputTarget(mAppWindow);
+        mAppWindow.mHasSurface = false;
+        mAppWindow.mActivityRecord.setVisibleRequested(false);
+        mAppWindow.mActivityRecord.setVisible(false);
+
+        final boolean[] foundIme = { false };
+        mDisplayContent.forAllWindows(w -> {
+            if (w == mImeWindow) {
+                foundIme[0] = true;
+            }
+        }, true /* traverseTopToBottom */);
+        assertTrue("IME must be found", foundIme[0]);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 41f1ac7..ea2abf7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -141,8 +141,8 @@
 import java.util.List;
 
 /** Common base class for window manager unit test classes. */
-class WindowTestsBase extends SystemServiceTestsBase {
-    final Context mContext = getInstrumentation().getTargetContext();
+public class WindowTestsBase extends SystemServiceTestsBase {
+    protected final Context mContext = getInstrumentation().getTargetContext();
 
     // Default package name
     static final String DEFAULT_COMPONENT_PACKAGE_NAME = "com.foo";
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
new file mode 100644
index 0000000..e5f2f89
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.wm.utils;
+
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE;
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_OFF;
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_ON;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.WindowTestRunner;
+import com.android.server.wm.WindowTestsBase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+
+/**
+ * Test class for [DesktopModeFlagsUtil]
+ *
+ * Build/Install/Run:
+ * atest WmTests:DesktopModeFlagsUtilTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DesktopModeFlagsUtilTest extends WindowTestsBase {
+
+    @Rule
+    public SetFlagsRule setFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setUp() throws Exception {
+        resetCache();
+    }
+
+    private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
+            "sys.wmshell.desktopmode.dev_toggle_override";
+
+    @Test
+    @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_OFF.getSetting());
+        // In absence of dev options, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+
+    @Test
+    @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For overridableFlag, for unset overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For overridableFlag, for unset overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_noOverride_featureFlagOn_returnsTrue() {
+        setOverride(null);
+
+        // For overridableFlag, in absence of overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noOverride_featureFlagOff_returnsFalse() {
+        setOverride(null);
+
+        // For overridableFlag, in absence of overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_unrecognizableOverride_featureFlagOn_returnsTrue() {
+        setOverride(-2);
+
+        // For overridableFlag, for unrecognized overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_unrecognizableOverride_featureFlagOff_returnsFalse() {
+        setOverride(-2);
+
+        // For overridableFlag, for unrecognizable overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideOff_featureFlagOn_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideOn_featureFlagOff_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Keep overrides constant through the process
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Keep overrides constant through the process
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(OVERRIDE_ON.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc");
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if currently invalid
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2");
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if currently invalid
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(
+                OVERRIDE_OFF.getSetting()));
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting()));
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY,
+                String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY})
+    public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    private void setOverride(Integer setting) {
+        ContentResolver contentResolver = mContext.getContentResolver();
+        String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES;
+
+        if (setting == null) {
+            Settings.Global.putString(contentResolver, key, null);
+        } else {
+            Settings.Global.putInt(contentResolver, key, setting);
+        }
+    }
+
+    private void resetCache() throws Exception {
+        Field cachedToggleOverride = DesktopModeFlagsUtil.class.getDeclaredField(
+                "sCachedToggleOverride");
+        cachedToggleOverride.setAccessible(true);
+        cachedToggleOverride.set(null, null);
+
+        // Clear override cache stored in System property
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+    }
+}
diff --git a/telephony/OWNERS b/telephony/OWNERS
index 7607c64..92af034 100644
--- a/telephony/OWNERS
+++ b/telephony/OWNERS
@@ -15,4 +15,4 @@
 per-file CarrierConfigManager.java=amruthr@google.com,tgunn@google.com,rgreenwalt@google.com,satk@google.com
 
 #Domain Selection is jointly owned, add additional owners for domain selection specific files
-per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com
+per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,jaesikkong@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index e839fc1..7739171 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -137,22 +137,25 @@
       diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg);
       return false;
     }
+
     std::vector<std::string> name_parts = util::Split(flag_name, ':');
     if (name_parts.size() > 2) {
       diag->Error(android::DiagMessage()
                   << "Invalid feature flag and optional value '" << flag_and_value
-                  << "'. Must be in the format 'flag_name[:ro][=true|false]");
+                  << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]");
       return false;
     }
     flag_name = name_parts[0];
     bool read_only = false;
     if (name_parts.size() == 2) {
-      if (name_parts[1] == "ro") {
+      if (name_parts[1] == "ro" || name_parts[1] == "READ_ONLY") {
         read_only = true;
+      } else if (name_parts[1] == "READ_WRITE") {
+        read_only = false;
       } else {
         diag->Error(android::DiagMessage()
                     << "Invalid feature flag and optional value '" << flag_and_value
-                    << "'. Must be in the format 'flag_name[:ro][=true|false]");
+                    << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]");
         return false;
       }
     }
diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp
index 35bc637..7818340 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -383,7 +383,7 @@
 TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) {
   auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
   FeatureFlagValues feature_flag_values;
-  ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar=true,foo:ro=false", diagnostics,
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar:READ_WRITE=true,foo:ro=false", diagnostics,
                                          &feature_flag_values));
   EXPECT_THAT(
       feature_flag_values,
@@ -394,11 +394,11 @@
 TEST(UtilTest, ParseFeatureFlagsParameter_Valid) {
   auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
   FeatureFlagValues feature_flag_values;
-  ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar:ro =FALSE,baz=, quux", diagnostics,
-                                         &feature_flag_values));
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo:READ_ONLY= true, bar:ro =FALSE,baz:READ_WRITE=, quux",
+                                         diagnostics, &feature_flag_values));
   EXPECT_THAT(
       feature_flag_values,
-      UnorderedElementsAre(Pair("foo", FeatureFlagProperties{false, std::optional<bool>(true)}),
+      UnorderedElementsAre(Pair("foo", FeatureFlagProperties{true, std::optional<bool>(true)}),
                            Pair("bar", FeatureFlagProperties{true, std::optional<bool>(false)}),
                            Pair("baz", FeatureFlagProperties{false, std::nullopt}),
                            Pair("quux", FeatureFlagProperties{false, std::nullopt})));