Merge "Clean up MEDIA_DEVICE_NAME_FIX flag" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8d480e5..de07fdde6 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -16834,6 +16834,10 @@
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.SatelliteCapabilities> CREATOR;
   }
 
+  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteCapabilitiesCallback {
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteCapabilitiesChanged(@NonNull android.telephony.satellite.SatelliteCapabilities);
+  }
+
   @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class SatelliteDatagram implements android.os.Parcelable {
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int describeContents();
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public byte[] getSatelliteDatagram();
@@ -16852,6 +16856,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteProvisionStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteProvisionStateCallback);
@@ -16872,6 +16877,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void startSatelliteTransmissionUpdates(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>, @NonNull android.telephony.satellite.SatelliteTransmissionUpdateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void stopSatelliteTransmissionUpdates(@NonNull android.telephony.satellite.SatelliteTransmissionUpdateCallback, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForNtnSignalStrengthChanged(@NonNull android.telephony.satellite.NtnSignalStrengthCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteCapabilitiesChanged(@NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteDatagram(@NonNull android.telephony.satellite.SatelliteDatagramCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteModemStateChanged(@NonNull android.telephony.satellite.SatelliteStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteProvisionStateChanged(@NonNull android.telephony.satellite.SatelliteProvisionStateCallback);
diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java
index 35509237..fd13174 100644
--- a/core/java/android/app/DownloadManager.java
+++ b/core/java/android/app/DownloadManager.java
@@ -575,8 +575,9 @@
                     extras.putString(Downloads.DIR_TYPE, dirType);
                     client.call(Downloads.CALL_CREATE_EXTERNAL_PUBLIC_DIR, null, extras);
                 } catch (RemoteException e) {
-                    throw new IllegalStateException("Unable to create directory: "
-                            + file.getAbsolutePath());
+                    throw new IllegalStateException(
+                        "Unable to create directory: " + file.getAbsolutePath(),
+                        e);
                 }
             } else {
                 if (file.exists()) {
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index b0332c3..ffb79b3 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1642,6 +1642,7 @@
      *
      * <p>
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public Policy getNotificationPolicy() {
         INotificationManager service = getService();
         try {
@@ -1660,6 +1661,7 @@
      *
      * @param policy The new desired policy.
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public void setNotificationPolicy(@NonNull Policy policy) {
         checkRequired("policy", policy);
         INotificationManager service = getService();
@@ -2608,6 +2610,7 @@
      * Only available if policy access is granted to this package. See
      * {@link #isNotificationPolicyAccessGranted}.
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
         final INotificationManager service = getService();
         try {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 4c70c91..3df11f6 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -81,6 +81,9 @@
 import android.app.IServiceConnection;
 import android.app.KeyguardManager;
 import android.app.admin.SecurityLog.SecurityEvent;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -9118,6 +9121,19 @@
     }
 
     /**
+     * For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, the
+     * {@link #isDeviceOwnerApp} method will use the user contained within the
+     * context.
+     * For apps targeting an SDK version <em>below</em> this, the user of the calling process will
+     * be used (Process.myUserHandle()).
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public static final long IS_DEVICE_OWNER_USER_AWARE = 307233716L;
+
+    /**
      * Used to determine if a particular package has been registered as a Device Owner app.
      * A device owner app is a special device admin that cannot be deactivated by the user, once
      * activated as a device admin. It also cannot be uninstalled. To check whether a particular
@@ -9130,8 +9146,13 @@
      * app, if any.
      * @return whether or not the package is registered as the device owner app.
      */
+    @UserHandleAware(enabledSinceTargetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public boolean isDeviceOwnerApp(String packageName) {
         throwIfParentInstance("isDeviceOwnerApp");
+        if (android.permission.flags.Flags.roleControllerInSystemServer()
+                && CompatChanges.isChangeEnabled(IS_DEVICE_OWNER_USER_AWARE)) {
+            return isDeviceOwnerAppOnContextUser(packageName);
+        }
         return isDeviceOwnerAppOnCallingUser(packageName);
     }
 
@@ -9192,6 +9213,24 @@
         return packageName.equals(deviceOwner.getPackageName());
     }
 
+    private boolean isDeviceOwnerAppOnContextUser(String packageName) {
+        if (packageName == null) {
+            return false;
+        }
+        ComponentName deviceOwner = null;
+        if (mService != null) {
+            try {
+                deviceOwner = mService.getDeviceOwnerComponentOnUser(myUserId());
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
+        }
+        if (deviceOwner == null) {
+            return false;
+        }
+        return packageName.equals(deviceOwner.getPackageName());
+    }
+
     private ComponentName getDeviceOwnerComponentInner(boolean callingUserOnly) {
         if (mService != null) {
             try {
@@ -9608,6 +9647,7 @@
      * @param packageName The package name of the app to compare with the registered profile owner.
      * @return Whether or not the package is registered as the profile owner.
      */
+    @UserHandleAware
     public boolean isProfileOwnerApp(String packageName) {
         throwIfParentInstance("isProfileOwnerApp");
         if (mService != null) {
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 6fe40be..575fa4c 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -179,6 +179,7 @@
 
     boolean setDeviceOwner(in ComponentName who, int userId, boolean setProfileOwnerOnCurrentUserIfNecessary);
     ComponentName getDeviceOwnerComponent(boolean callingUserOnly);
+    ComponentName getDeviceOwnerComponentOnUser(int userId);
     boolean hasDeviceOwner();
     String getDeviceOwnerName();
     void clearDeviceOwner(String packageName);
diff --git a/core/java/android/content/pm/SigningInfo.java b/core/java/android/content/pm/SigningInfo.java
index 543703e..a407704 100644
--- a/core/java/android/content/pm/SigningInfo.java
+++ b/core/java/android/content/pm/SigningInfo.java
@@ -190,9 +190,11 @@
         mSigningDetails.writeToParcel(dest, parcelableFlags);
     }
 
-    /* @hide */
+    /**
+     *  @hide
+     */
     @NonNull
-    SigningDetails getSigningDetails() {
+    public SigningDetails getSigningDetails() {
         return mSigningDetails;
     }
 
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index ff4dfc7..305b751 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1004,6 +1004,8 @@
             priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
             conversationSenders = getConversationSendersWithDefault(
                     zenPolicy.getPriorityConversationSenders(), conversationSenders);
+        } else {
+            conversationSenders = CONVERSATION_SENDERS_NONE;
         }
 
         if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CALLS,
@@ -1102,7 +1104,7 @@
         return (policy.suppressedVisualEffects & visualEffect) == 0;
     }
 
-    private int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
+    private static int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
             int defaultPolicySender) {
         switch (senders) {
             case ZenPolicy.PEOPLE_TYPE_ANYONE:
@@ -1116,7 +1118,7 @@
         }
     }
 
-    private int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders,
+    private static int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders,
             int defaultPolicySender) {
         switch (senders) {
             case ZenPolicy.CONVERSATION_SENDERS_ANYONE:
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
new file mode 100644
index 0000000..b63a969
--- /dev/null
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.window.flags"
+
+flag {
+  name: "allows_screen_size_decoupled_from_status_bar_and_cutout"
+  namespace: "large_screen_experiences_app_compat"
+  description: "When necessary, configuration decoupled from status bar and display cutout"
+  bug: "291870756"
+  is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/res/OWNERS b/core/res/OWNERS
index 0df7c20..f24c3f5 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -1,5 +1,6 @@
 adamp@google.com
 asc@google.com
+austindelgado@google.com
 cinek@google.com
 dsandler@android.com
 dsandler@google.com
@@ -8,6 +9,7 @@
 hackbod@google.com
 ilyamaty@google.com
 jaggies@google.com
+jbolinger@google.com
 jsharkey@android.com
 jsharkey@google.com
 juliacr@google.com
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 15c188d..cec83de 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5245,6 +5245,11 @@
     <!-- Zen mode - name of default automatic calendar time-based rule that is triggered every night (when sleeping). [CHAR LIMIT=40] -->
     <string name="zen_mode_default_every_night_name">Sleeping</string>
 
+    <!-- Zen mode - Condition summary when a rule is activated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
+    <string name="zen_mode_implicit_activated">On</string>
+    <!-- Zen mode - Condition summary when a rule is deactivated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
+    <string name="zen_mode_implicit_deactivated">Off</string>
+
     <!-- Indication that the current volume and other effects (vibration) are being suppressed by a third party, such as a notification listener. [CHAR LIMIT=30] -->
     <string name="muted_by"><xliff:g id="third_party">%1$s</xliff:g> is muting some sounds</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e19d548..16ad5c9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2574,6 +2574,8 @@
   <java-symbol type="string" name="zen_mode_default_weekends_name" />
   <java-symbol type="string" name="zen_mode_default_events_name" />
   <java-symbol type="string" name="zen_mode_default_every_night_name" />
+  <java-symbol type="string" name="zen_mode_implicit_activated" />
+  <java-symbol type="string" name="zen_mode_implicit_deactivated" />
   <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
   <java-symbol type="string" name="display_rotation_camera_compat_toast_in_multi_window" />
   <java-symbol type="array" name="config_system_condition_providers" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index d5b29e3..d5fab44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -822,14 +822,23 @@
                     + "participant");
         }
 
-        // Make sure other open changes are visible as entering PIP. Some may be hidden in
-        // Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
+        // Make sure other non-pip changes are handled correctly.
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
             if (change == enterPip) continue;
             if (TransitionUtil.isOpeningType(change.getMode())) {
+                // For other open changes that are visible when entering PIP, some may be hidden in
+                // Transitions#setupStartState because the transition type is OPEN (such as
+                // auto-enter).
                 final SurfaceControl leash = change.getLeash();
                 startTransaction.show(leash).setAlpha(leash, 1.f);
+            } else if (TransitionUtil.isClosingType(change.getMode())) {
+                // For other close changes that are invisible as entering PIP, hide them immediately
+                // to avoid showing a freezing surface.
+                // Ideally, we should let other handler to handle them (likely RemoteHandler by
+                // Launcher).
+                final SurfaceControl leash = change.getLeash();
+                startTransaction.hide(leash);
             }
         }
 
diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java
index 5a27435..2b349d4 100644
--- a/media/java/android/media/AudioDeviceAttributes.java
+++ b/media/java/android/media/AudioDeviceAttributes.java
@@ -325,7 +325,7 @@
                 + " role:" + roleToString(mRole)
                 + " type:" + (mRole == ROLE_OUTPUT ? AudioSystem.getOutputDeviceName(mNativeType)
                         : AudioSystem.getInputDeviceName(mNativeType))
-                + " addr:" + mAddress
+                + " addr:" + Utils.anonymizeBluetoothAddress(mNativeType, mAddress)
                 + " name:" + mName
                 + " profiles:" + mAudioProfiles.toString()
                 + " descriptors:" + mAudioDescriptors.toString());
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 9211c53..73bc6f9 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -156,7 +156,7 @@
                             AudioSystem.getOutputDeviceName(mType));
         return "{" + super.toString()
                 + ", mType: " + type
-                + ", mAddress: " + mAddress
+                + ", mAddress: " + Utils.anonymizeBluetoothAddress(mType, mAddress)
                 + "}";
     }
 }
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index ecb6b3d..d07f611 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -657,4 +657,35 @@
         // on the fly.
         private final boolean mForceRemoveConsistency; // default false
     }
+
+    /**
+     * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
+     * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
+     * @param address MAC address to be anonymized
+     * @return anonymized MAC address
+     */
+    public static @Nullable String anonymizeBluetoothAddress(@Nullable String address) {
+        if (address == null) {
+            return null;
+        }
+        if (address.length() != "AA:BB:CC:DD:EE:FF".length()) {
+            return address;
+        }
+        return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
+    }
+
+    /**
+     * Convert a Bluetooth MAC address to an anonymized one if the internal device type corresponds
+     * to a Bluetooth.
+     * @param deviceType the internal type of the audio device
+     * @param address MAC address to be anonymized
+     * @return anonymized MAC address
+     */
+    public static @Nullable String anonymizeBluetoothAddress(
+            int deviceType, @Nullable String address) {
+        if (!AudioSystem.isBluetoothDevice(deviceType)) {
+            return address;
+        }
+        return anonymizeBluetoothAddress(address);
+    }
 }
diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.aidl b/media/java/android/media/tv/ad/TvAdServiceInfo.aidl
new file mode 100644
index 0000000..a5e631f
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdServiceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+parcelable TvAdServiceInfo;
diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java
new file mode 100644
index 0000000..ed04f1f
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdServiceInfo.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import androidx.annotation.NonNull;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is used to specify meta information of a TV AD service.
+ * @hide
+ */
+public final class TvAdServiceInfo implements Parcelable {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "TvAdServiceInfo";
+
+    private static final String XML_START_TAG_NAME = "tv-ad-service";
+
+    private final ResolveInfo mService;
+    private final String mId;
+    private final List<String> mTypes = new ArrayList<>();
+
+    /**
+     * Constructs a TvAdServiceInfo object.
+     *
+     * @param context the application context
+     * @param component the component name of the TvAdService
+     */
+    public TvAdServiceInfo(@NonNull Context context, @NonNull ComponentName component) {
+        if (context == null) {
+            throw new IllegalArgumentException("context cannot be null.");
+        }
+        // TODO: use a constant
+        Intent intent = new Intent("android.media.tv.ad.TvAdService").setComponent(component);
+        ResolveInfo resolveInfo = context.getPackageManager().resolveService(
+                intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+        if (resolveInfo == null) {
+            throw new IllegalArgumentException("Invalid component. Can't find the service.");
+        }
+
+        ComponentName componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
+                resolveInfo.serviceInfo.name);
+        String id;
+        id = generateAdServiceId(componentName);
+        List<String> types = new ArrayList<>();
+        parseServiceMetadata(resolveInfo, context, types);
+
+        mService = resolveInfo;
+        mId = id;
+    }
+
+    private TvAdServiceInfo(ResolveInfo service, String id, List<String> types) {
+        mService = service;
+        mId = id;
+        mTypes.addAll(types);
+    }
+
+    private TvAdServiceInfo(@NonNull Parcel in) {
+        mService = ResolveInfo.CREATOR.createFromParcel(in);
+        mId = in.readString();
+        in.readStringList(mTypes);
+    }
+
+    public static final Creator<TvAdServiceInfo> CREATOR = new Creator<TvAdServiceInfo>() {
+        @Override
+        public TvAdServiceInfo createFromParcel(Parcel in) {
+            return new TvAdServiceInfo(in);
+        }
+
+        @Override
+        public TvAdServiceInfo[] newArray(int size) {
+            return new TvAdServiceInfo[size];
+        }
+    };
+
+    /**
+     * Returns a unique ID for this TV AD service. The ID is generated from the package and class
+     * name implementing the TV AD service.
+     */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the component of the TV AD service.
+     * @hide
+     */
+    public ComponentName getComponent() {
+        return new ComponentName(mService.serviceInfo.packageName, mService.serviceInfo.name);
+    }
+
+    /**
+     * Returns the information of the service that implements this AD service.
+     */
+    @Nullable
+    public ServiceInfo getServiceInfo() {
+        return mService.serviceInfo;
+    }
+
+    /**
+     * Gets supported TV AD types.
+     */
+    @NonNull
+    public List<String> getSupportedTypes() {
+        return mTypes;
+    }
+
+    private static String generateAdServiceId(ComponentName name) {
+        return name.flattenToShortString();
+    }
+
+    private static void parseServiceMetadata(
+            ResolveInfo resolveInfo, Context context, List<String> types) {
+        ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+        PackageManager pm = context.getPackageManager();
+        // TODO: use constant for the metadata
+        try (XmlResourceParser parser =
+                     serviceInfo.loadXmlMetaData(pm, "android.media.tv.ad.service")) {
+            if (parser == null) {
+                throw new IllegalStateException(
+                        "No " + "android.media.tv.ad.service"
+                                + " meta-data found for " + serviceInfo.name);
+            }
+
+            Resources resources = pm.getResourcesForApplication(serviceInfo.applicationInfo);
+            AttributeSet attrs = Xml.asAttributeSet(parser);
+
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && type != XmlPullParser.START_TAG) {
+                // move to the START_TAG
+            }
+
+            String nodeName = parser.getName();
+            if (!XML_START_TAG_NAME.equals(nodeName)) {
+                throw new IllegalStateException("Meta-data does not start with "
+                        + XML_START_TAG_NAME + " tag for " + serviceInfo.name);
+            }
+
+            // TODO: parse attributes
+        } catch (IOException | XmlPullParserException e) {
+            throw new IllegalStateException(
+                    "Failed reading meta-data for " + serviceInfo.packageName, e);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new IllegalStateException("No resources found for " + serviceInfo.packageName, e);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        mService.writeToParcel(dest, flags);
+        dest.writeString(mId);
+        dest.writeStringList(mTypes);
+    }
+}
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch.xml
new file mode 100644
index 0000000..1ffdad4
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<Switch
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/switch_widget"
+    android:layout_width="wrap_content"
+    android:layout_height="48dp"
+    android:clickable="false"
+    android:focusable="false"
+    android:theme="@style/Switch.SettingsLib" />
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
index e3f8fbb..1054e00 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
@@ -47,14 +47,7 @@
             android:textAppearance="?android:attr/textAppearanceListItem"
             style="@style/MainSwitchText.Settingslib" />
 
-        <Switch
-            android:id="@android:id/switch_widget"
-            android:layout_width="wrap_content"
-            android:layout_height="48dp"
-            android:layout_gravity="center_vertical"
-            android:focusable="false"
-            android:clickable="false"
-            android:theme="@style/Switch.SettingsLib"/>
+        <include layout="@layout/settingslib_main_switch" />
     </LinearLayout>
 
 </LinearLayout>
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
index 255b2c9..4a0e7b3 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
@@ -49,14 +49,7 @@
             android:lineBreakWordStyle="phrase"
             style="@style/MainSwitchText.Settingslib" />
 
-        <Switch
-            android:id="@android:id/switch_widget"
-            android:layout_width="wrap_content"
-            android:layout_height="48dp"
-            android:layout_gravity="center_vertical"
-            android:focusable="false"
-            android:clickable="false"
-            android:theme="@style/Switch.SettingsLib"/>
+        <include layout="@layout/settingslib_main_switch" />
     </LinearLayout>
 
 </LinearLayout>
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch.xml
new file mode 100644
index 0000000..12c1d76
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<Switch
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/switch_widget"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:clickable="false"
+    android:focusable="false"
+    android:theme="@style/SwitchBar.Switch.Settingslib" />
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
index bf34db9..fe64fea 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
@@ -38,13 +38,6 @@
         android:layout_marginStart="@dimen/settingslib_switchbar_subsettings_margin_start"
         android:textAlignment="viewStart"/>
 
-    <Switch
-        android:id="@android:id/switch_widget"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_vertical"
-        android:focusable="false"
-        android:clickable="false"
-        android:theme="@style/SwitchBar.Switch.Settingslib"/>
+    <include layout="@layout/settingslib_main_switch" />
 </LinearLayout>
 
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
index 1c92696..223e99e 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -57,19 +57,24 @@
 
 private fun UserManager.getUserGroups(): List<UserGroup> {
     val userGroupList = mutableListOf<UserGroup>()
-    val profileToShowInSettings = getProfiles(UserHandle.myUserId())
-        .map { userInfo -> userInfo to getUserProperties(userInfo.userHandle) }
+    val showInSettingsMap = getProfiles(UserHandle.myUserId()).groupBy { showInSettings(it) }
 
-    profileToShowInSettings
-        .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_WITH_PARENT }
-        .takeIf { it.isNotEmpty() }
-        ?.map { it.first }
-        ?.let { userInfos -> userGroupList += UserGroup(userInfos) }
+    showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_WITH_PARENT]?.let {
+        userGroupList += UserGroup(it)
+    }
 
-    profileToShowInSettings
-        .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_SEPARATE &&
-                    (!it.second.hideInSettingsInQuietMode || !it.first.isQuietModeEnabled) }
-        .forEach { userGroupList += UserGroup(userInfos = listOf(it.first)) }
+    showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_SEPARATE]?.forEach {
+        userGroupList += UserGroup(listOf(it))
+    }
 
     return userGroupList
 }
+
+private fun UserManager.showInSettings(userInfo: UserInfo): Int {
+    val userProperties = getUserProperties(userInfo.userHandle)
+    return if (userInfo.isQuietModeEnabled && userProperties.hideInSettingsInQuietMode) {
+        UserProperties.SHOW_IN_SETTINGS_NO
+    } else {
+        userProperties.showInSettings
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt
new file mode 100644
index 0000000..e450364
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.settingslib.spaprivileged.template.common
+
+import android.content.Context
+import android.content.pm.UserInfo
+import android.content.pm.UserProperties
+import android.os.UserManager
+import androidx.compose.material3.Text
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+
+@RunWith(AndroidJUnit4::class)
+class UserProfilePagerTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val mockUserManager = mock<UserManager> {
+        on { getProfiles(any()) } doReturn listOf(USER_0)
+        on { getUserProperties(USER_0.userHandle) } doReturn
+            UserProperties.Builder()
+                .setShowInSettings(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
+                .build()
+    }
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { userManager } doReturn mockUserManager
+    }
+
+    @Test
+    fun userProfilePager() {
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                UserProfilePager { userGroup ->
+                    Text(text = userGroup.userInfos.joinToString { it.id.toString() })
+                }
+            }
+        }
+
+        composeTestRule.onNodeWithText(USER_0.id.toString()).assertIsDisplayed()
+    }
+
+    private companion object {
+        val USER_0 = UserInfo(0, "", 0)
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 33c084e..3928767 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -78,7 +78,10 @@
 import androidx.compose.ui.unit.times
 import com.android.compose.PlatformButton
 import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneKey as SceneTransitionLayoutSceneKey
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.transitions
 import com.android.compose.modifiers.thenIf
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
@@ -90,6 +93,8 @@
 import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.fold.ui.composable.FoldPosture
+import com.android.systemui.fold.ui.composable.foldPosture
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
@@ -159,28 +164,27 @@
 
         when (layout) {
             Layout.STANDARD ->
-                Bouncer(
+                StandardLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
-                    userInputAreaVisibility = UserInputAreaVisibility.FULL,
                     modifier = childModifier,
                 )
             Layout.SIDE_BY_SIDE ->
-                SideBySide(
+                SideBySideLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
                     isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
                     modifier = childModifier,
                 )
             Layout.STACKED ->
-                Stacked(
+                StackedLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
                     isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
                     modifier = childModifier,
                 )
             Layout.SPLIT ->
-                Split(
+                SplitLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
                     modifier = childModifier,
@@ -194,59 +198,150 @@
  * authentication attempt, including all messaging UI (directives, reasoning, errors, etc.).
  */
 @Composable
-private fun Bouncer(
+private fun StandardLayout(
     viewModel: BouncerViewModel,
     dialogFactory: BouncerSceneDialogFactory,
-    userInputAreaVisibility: UserInputAreaVisibility,
+    modifier: Modifier = Modifier,
+    outputOnly: Boolean = false,
+) {
+    val foldPosture: FoldPosture by foldPosture()
+    val isSplitAroundTheFoldRequired by viewModel.isFoldSplitRequired.collectAsState()
+    val isSplitAroundTheFold =
+        foldPosture == FoldPosture.Tabletop && !outputOnly && isSplitAroundTheFoldRequired
+    val currentSceneKey by
+        remember(isSplitAroundTheFold) {
+            mutableStateOf(
+                if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey
+            )
+        }
+
+    SceneTransitionLayout(
+        currentScene = currentSceneKey,
+        onChangeScene = {},
+        transitions = SceneTransitions,
+        modifier = modifier,
+    ) {
+        scene(SceneKeys.ContiguousSceneKey) {
+            FoldSplittable(
+                viewModel = viewModel,
+                dialogFactory = dialogFactory,
+                outputOnly = outputOnly,
+                isSplit = false,
+            )
+        }
+
+        scene(SceneKeys.SplitSceneKey) {
+            FoldSplittable(
+                viewModel = viewModel,
+                dialogFactory = dialogFactory,
+                outputOnly = outputOnly,
+                isSplit = true,
+            )
+        }
+    }
+}
+
+/**
+ * Renders the "standard" layout of the bouncer, where the bouncer is rendered on its own (no user
+ * switcher UI) and laid out vertically, centered horizontally.
+ *
+ * If [isSplit] is `true`, the top and bottom parts of the bouncer are split such that they don't
+ * render across the location of the fold hardware when the device is fully or part-way unfolded
+ * with the fold hinge in a horizontal position.
+ *
+ * If [outputOnly] is `true`, only the "output" part of the UI is shown (where the entered PIN
+ * "shapes" appear), if `false`, the entire UI is shown, including the area where the user can enter
+ * their PIN or pattern.
+ */
+@Composable
+private fun SceneScope.FoldSplittable(
+    viewModel: BouncerViewModel,
+    dialogFactory: BouncerSceneDialogFactory,
+    outputOnly: Boolean,
+    isSplit: Boolean,
     modifier: Modifier = Modifier,
 ) {
     val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
     val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
     var dialog: Dialog? by remember { mutableStateOf(null) }
     val actionButton: BouncerActionButtonModel? by viewModel.actionButton.collectAsState()
+    val splitRatio =
+        LocalContext.current.resources.getFloat(
+            R.dimen.motion_layout_half_fold_bouncer_height_ratio
+        )
 
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        modifier = modifier.padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 0.dp)
-    ) {
-        Crossfade(
-            targetState = message,
-            label = "Bouncer message",
-            animationSpec = if (message.isUpdateAnimated) tween() else snap(),
-        ) { message ->
-            Text(
-                text = message.text,
-                color = MaterialTheme.colorScheme.onSurface,
-                style = MaterialTheme.typography.bodyLarge,
-            )
-        }
+    Column(modifier = modifier.padding(horizontal = 32.dp)) {
+        // Content above the fold, when split on a foldable device in a "table top" posture:
+        Box(
+            modifier =
+                Modifier.element(SceneElements.AboveFold).fillMaxWidth().thenIf(isSplit) {
+                    Modifier.weight(splitRatio)
+                },
+        ) {
+            Column(
+                horizontalAlignment = Alignment.CenterHorizontally,
+                modifier = Modifier.fillMaxWidth().padding(top = 92.dp),
+            ) {
+                Crossfade(
+                    targetState = message,
+                    label = "Bouncer message",
+                    animationSpec = if (message.isUpdateAnimated) tween() else snap(),
+                ) { message ->
+                    Text(
+                        text = message.text,
+                        color = MaterialTheme.colorScheme.onSurface,
+                        style = MaterialTheme.typography.bodyLarge,
+                    )
+                }
 
-        Spacer(Modifier.heightIn(min = 21.dp, max = 48.dp))
+                Spacer(Modifier.heightIn(min = 21.dp, max = 48.dp))
 
-        Box(Modifier.weight(1f)) {
-            UserInputArea(
-                viewModel = viewModel,
-                visibility = userInputAreaVisibility,
-                modifier = Modifier.align(Alignment.Center),
-            )
-        }
-
-        Spacer(Modifier.heightIn(min = 21.dp, max = 48.dp))
-
-        val actionButtonModifier = Modifier.height(56.dp)
-
-        actionButton.let { actionButtonViewModel ->
-            if (actionButtonViewModel != null) {
-                BouncerActionButton(
-                    viewModel = actionButtonViewModel,
-                    modifier = actionButtonModifier,
+                UserInputArea(
+                    viewModel = viewModel,
+                    visibility = UserInputAreaVisibility.OUTPUT_ONLY,
                 )
-            } else {
-                Spacer(modifier = actionButtonModifier)
             }
         }
 
-        Spacer(Modifier.height(48.dp))
+        // Content below the fold, when split on a foldable device in a "table top" posture:
+        Box(
+            modifier =
+                Modifier.element(SceneElements.BelowFold).fillMaxWidth().thenIf(isSplit) {
+                    Modifier.weight(1 - splitRatio)
+                },
+        ) {
+            Column(
+                horizontalAlignment = Alignment.CenterHorizontally,
+                modifier = Modifier.fillMaxWidth(),
+            ) {
+                if (!outputOnly) {
+                    Box(Modifier.weight(1f)) {
+                        UserInputArea(
+                            viewModel = viewModel,
+                            visibility = UserInputAreaVisibility.INPUT_ONLY,
+                            modifier = Modifier.align(Alignment.Center),
+                        )
+                    }
+                }
+
+                Spacer(Modifier.heightIn(min = 21.dp, max = 48.dp))
+
+                val actionButtonModifier = Modifier.height(56.dp)
+
+                actionButton.let { actionButtonViewModel ->
+                    if (actionButtonViewModel != null) {
+                        BouncerActionButton(
+                            viewModel = actionButtonViewModel,
+                            modifier = actionButtonModifier,
+                        )
+                    } else {
+                        Spacer(modifier = actionButtonModifier)
+                    }
+                }
+
+                Spacer(Modifier.height(48.dp))
+            }
+        }
 
         if (dialogMessage != null) {
             if (dialog == null) {
@@ -288,8 +383,8 @@
     when (val nonNullViewModel = authMethodViewModel) {
         is PinBouncerViewModel ->
             when (visibility) {
-                UserInputAreaVisibility.FULL ->
-                    PinBouncer(
+                UserInputAreaVisibility.OUTPUT_ONLY ->
+                    PinInputDisplay(
                         viewModel = nonNullViewModel,
                         modifier = modifier,
                     )
@@ -298,34 +393,21 @@
                         viewModel = nonNullViewModel,
                         modifier = modifier,
                     )
-                UserInputAreaVisibility.OUTPUT_ONLY ->
-                    PinInputDisplay(
-                        viewModel = nonNullViewModel,
-                        modifier = modifier,
-                    )
-                UserInputAreaVisibility.NONE -> {}
             }
         is PasswordBouncerViewModel ->
-            when (visibility) {
-                UserInputAreaVisibility.FULL,
-                UserInputAreaVisibility.INPUT_ONLY ->
-                    PasswordBouncer(
-                        viewModel = nonNullViewModel,
-                        modifier = modifier,
-                    )
-                else -> {}
+            if (visibility == UserInputAreaVisibility.INPUT_ONLY) {
+                PasswordBouncer(
+                    viewModel = nonNullViewModel,
+                    modifier = modifier,
+                )
             }
         is PatternBouncerViewModel ->
-            when (visibility) {
-                UserInputAreaVisibility.FULL,
-                UserInputAreaVisibility.INPUT_ONLY ->
-                    PatternBouncer(
-                        viewModel = nonNullViewModel,
-                        modifier =
-                            Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
-                                .then(modifier)
-                    )
-                else -> {}
+            if (visibility == UserInputAreaVisibility.INPUT_ONLY) {
+                PatternBouncer(
+                    viewModel = nonNullViewModel,
+                    modifier =
+                        Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false).then(modifier)
+                )
             }
         else -> Unit
     }
@@ -492,17 +574,17 @@
  * by double-tapping on the side.
  */
 @Composable
-private fun Split(
+private fun SplitLayout(
     viewModel: BouncerViewModel,
     dialogFactory: BouncerSceneDialogFactory,
     modifier: Modifier = Modifier,
 ) {
     SwappableLayout(
         startContent = { startContentModifier ->
-            Bouncer(
+            StandardLayout(
                 viewModel = viewModel,
                 dialogFactory = dialogFactory,
-                userInputAreaVisibility = UserInputAreaVisibility.OUTPUT_ONLY,
+                outputOnly = true,
                 modifier = startContentModifier,
             )
         },
@@ -595,7 +677,7 @@
  * rendering of the bouncer will be used instead of the side-by-side layout.
  */
 @Composable
-private fun SideBySide(
+private fun SideBySideLayout(
     viewModel: BouncerViewModel,
     dialogFactory: BouncerSceneDialogFactory,
     isUserSwitcherVisible: Boolean,
@@ -615,10 +697,9 @@
             }
         },
         endContent = { endContentModifier ->
-            Bouncer(
+            StandardLayout(
                 viewModel = viewModel,
                 dialogFactory = dialogFactory,
-                userInputAreaVisibility = UserInputAreaVisibility.FULL,
                 modifier = endContentModifier,
             )
         },
@@ -628,7 +709,7 @@
 
 /** Arranges the bouncer contents and user switcher contents one on top of the other, vertically. */
 @Composable
-private fun Stacked(
+private fun StackedLayout(
     viewModel: BouncerViewModel,
     dialogFactory: BouncerSceneDialogFactory,
     isUserSwitcherVisible: Boolean,
@@ -644,10 +725,9 @@
             )
         }
 
-        Bouncer(
+        StandardLayout(
             viewModel = viewModel,
             dialogFactory = dialogFactory,
-            userInputAreaVisibility = UserInputAreaVisibility.FULL,
             modifier = Modifier.fillMaxWidth().weight(1f),
         )
     }
@@ -708,11 +788,6 @@
 /** Enumerates all supported user-input area visibilities. */
 private enum class UserInputAreaVisibility {
     /**
-     * The entire user input area is shown, including where the user enters input and where it's
-     * reflected to the user.
-     */
-    FULL,
-    /**
      * Only the area where the user enters the input is shown; the area where the input is reflected
      * back to the user is not shown.
      */
@@ -722,8 +797,6 @@
      * input is entered by the user is not shown.
      */
     OUTPUT_ONLY,
-    /** The entire user input area is hidden. */
-    NONE,
 }
 
 /**
@@ -758,3 +831,17 @@
 private val SelectedUserImageSize = 190.dp
 private val UserSwitcherDropdownWidth = SelectedUserImageSize + 2 * 29.dp
 private val UserSwitcherDropdownHeight = 60.dp
+
+private object SceneKeys {
+    val ContiguousSceneKey = SceneTransitionLayoutSceneKey("default")
+    val SplitSceneKey = SceneTransitionLayoutSceneKey("split")
+}
+
+private object SceneElements {
+    val AboveFold = ElementKey("above_fold")
+    val BelowFold = ElementKey("below_fold")
+}
+
+private val SceneTransitions = transitions {
+    from(SceneKeys.ContiguousSceneKey, to = SceneKeys.SplitSceneKey) { spec = tween() }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 5b9ad4d..fb50f69 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -24,14 +24,10 @@
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -69,34 +65,13 @@
 import kotlinx.coroutines.launch
 
 @Composable
-internal fun PinBouncer(
+fun PinPad(
     viewModel: PinBouncerViewModel,
     modifier: Modifier = Modifier,
 ) {
     // Report that the UI is shown to let the view-model run some logic.
     LaunchedEffect(Unit) { viewModel.onShown() }
 
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        modifier =
-            modifier.pointerInput(Unit) {
-                awaitEachGesture {
-                    awaitFirstDown()
-                    viewModel.onDown()
-                }
-            }
-    ) {
-        PinInputDisplay(viewModel)
-        Spacer(Modifier.heightIn(min = 34.dp, max = 48.dp))
-        PinPad(viewModel)
-    }
-}
-
-@Composable
-fun PinPad(
-    viewModel: PinBouncerViewModel,
-    modifier: Modifier = Modifier,
-) {
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
     val confirmButtonAppearance by viewModel.confirmButtonAppearance.collectAsState()
@@ -298,7 +273,8 @@
         contentAlignment = Alignment.Center,
         modifier =
             modifier
-                .size(pinButtonSize)
+                .sizeIn(maxWidth = pinButtonSize, maxHeight = pinButtonSize)
+                .aspectRatio(1f)
                 .drawBehind {
                     drawRoundRect(
                         color = containerColor,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
new file mode 100644
index 0000000..1c993cf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.fold.ui.composable
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowInfoTracker
+
+sealed interface FoldPosture {
+    /** A foldable device that's fully closed/folded or a device that doesn't support folding. */
+    data object Folded : FoldPosture
+    /** A foldable that's halfway open with the hinge held vertically. */
+    data object Book : FoldPosture
+    /** A foldable that's halfway open with the hinge held horizontally. */
+    data object Tabletop : FoldPosture
+    /** A foldable that's fully unfolded / flat. */
+    data object FullyUnfolded : FoldPosture
+}
+
+/** Returns the [FoldPosture] of the device currently. */
+@Composable
+fun foldPosture(): State<FoldPosture> {
+    val context = LocalContext.current
+    val infoTracker = remember(context) { WindowInfoTracker.getOrCreate(context) }
+    val layoutInfo by infoTracker.windowLayoutInfo(context).collectAsState(initial = null)
+
+    return produceState<FoldPosture>(
+        initialValue = FoldPosture.Folded,
+        key1 = layoutInfo,
+    ) {
+        value =
+            layoutInfo
+                ?.displayFeatures
+                ?.firstNotNullOfOrNull { it as? FoldingFeature }
+                .let { foldingFeature ->
+                    when (foldingFeature?.state) {
+                        null -> FoldPosture.Folded
+                        FoldingFeature.State.HALF_OPENED ->
+                            foldingFeature.orientation.toHalfwayPosture()
+                        FoldingFeature.State.FLAT ->
+                            if (foldingFeature.isSeparating) {
+                                // Dual screen device.
+                                foldingFeature.orientation.toHalfwayPosture()
+                            } else {
+                                FoldPosture.FullyUnfolded
+                            }
+                        else -> error("Unsupported state \"${foldingFeature.state}\"")
+                    }
+                }
+    }
+}
+
+private fun FoldingFeature.Orientation.toHalfwayPosture(): FoldPosture {
+    return when (this) {
+        FoldingFeature.Orientation.HORIZONTAL -> FoldPosture.Tabletop
+        FoldingFeature.Orientation.VERTICAL -> FoldPosture.Book
+        else -> error("Unsupported orientation \"$this\"")
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index aae61bd..b77a60b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -506,7 +506,10 @@
 
     // There is no ongoing transition.
     if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
-        return idleValue
+        // Even if this element SceneTransitionLayout is not animated, the layout itself might be
+        // animated (e.g. by another parent SceneTransitionLayout), in which case this element still
+        // need to participate in the layout phase.
+        return currentValue()
     }
 
     // A transition was started but it's not ready yet (not all elements have been composed/laid
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index 216608a..5d8eaf7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,9 +2,10 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.unit.IntSize
 
 interface DraggableHandler {
-    fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
+    fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int = 1)
     fun onDelta(pixels: Float)
     fun onDragStopped(velocity: Float)
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index d0a5f5b..d48781a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.util.fastForEach
 
@@ -60,7 +61,7 @@
     orientation: Orientation,
     enabled: Boolean,
     startDragImmediately: Boolean,
-    onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
+    onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
     onDragDelta: (Float) -> Unit,
     onDragStopped: (velocity: Float) -> Unit,
 ): Modifier = composed {
@@ -83,7 +84,7 @@
 
         val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
             velocityTracker.resetTracking()
-            onDragStarted(startedPosition, pointersDown)
+            onDragStarted(size, startedPosition, pointersDown)
         }
 
         val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index eb5168b..1a79522 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -25,8 +25,9 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.intermediateLayout
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.zIndex
@@ -44,14 +45,24 @@
     var content by mutableStateOf(content)
     var userActions by mutableStateOf(actions)
     var zIndex by mutableFloatStateOf(zIndex)
-    var size by mutableStateOf(IntSize.Zero)
+    var targetSize by mutableStateOf(IntSize.Zero)
 
     /** The shared values in this scene that are not tied to a specific element. */
     val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>()
 
     @Composable
+    @OptIn(ExperimentalComposeUiApi::class)
     fun Content(modifier: Modifier = Modifier) {
-        Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) {
+        Box(
+            modifier
+                .zIndex(zIndex)
+                .intermediateLayout { measurable, constraints ->
+                    targetSize = lookaheadSize
+                    val placeable = measurable.measure(constraints)
+                    layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+                }
+                .testTag(key.testTag)
+        ) {
             scope.content()
         }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 9a3a0ae..9d71801 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
@@ -90,7 +91,7 @@
 
     internal var gestureWithPriority: Any? = null
 
-    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
+    internal fun onDragStarted(pointersDown: Int, layoutSize: IntSize, startedPosition: Offset?) {
         if (isDrivingTransition) {
             // This [transition] was already driving the animation: simply take over it.
             // Stop animating and start from where the current offset.
@@ -126,14 +127,14 @@
         // we will also have to make sure that we correctly handle overscroll.
         swipeTransition.absoluteDistance =
             when (orientation) {
-                Orientation.Horizontal -> layoutImpl.size.width
-                Orientation.Vertical -> layoutImpl.size.height
+                Orientation.Horizontal -> layoutSize.width
+                Orientation.Vertical -> layoutSize.height
             }.toFloat()
 
         val fromEdge =
             startedPosition?.let { position ->
                 layoutImpl.edgeDetector.edge(
-                    layoutImpl.size,
+                    layoutSize,
                     position.round(),
                     layoutImpl.density,
                     orientation,
@@ -513,9 +514,9 @@
 private class SceneDraggableHandler(
     private val gestureHandler: SceneGestureHandler,
 ) : DraggableHandler {
-    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
+    override fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) {
         gestureHandler.gestureWithPriority = this
-        gestureHandler.onDragStarted(pointersDown, startedPosition)
+        gestureHandler.onDragStarted(pointersDown, layoutSize, startedPosition)
     }
 
     override fun onDelta(pixels: Float) {
@@ -647,7 +648,11 @@
             canContinueScroll = { true },
             onStart = {
                 gestureHandler.gestureWithPriority = this
-                gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
+                gestureHandler.onDragStarted(
+                    pointersDown = 1,
+                    layoutSize = gestureHandler.currentScene.targetSize,
+                    startedPosition = null,
+                )
             },
             onScroll = { offsetAvailable ->
                 if (gestureHandler.gestureWithPriority != this) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 6edd1b6..0b06953 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -30,13 +30,15 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.intermediateLayout
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
+import com.android.compose.ui.util.lerp
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 
@@ -64,12 +66,6 @@
     private val horizontalGestureHandler: SceneGestureHandler
     private val verticalGestureHandler: SceneGestureHandler
 
-    /**
-     * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
-     * any scene configured or right before the first measure pass of the layout.
-     */
-    @VisibleForTesting var size by mutableStateOf(IntSize.Zero)
-
     init {
         setScenes(builder)
 
@@ -157,15 +153,46 @@
     }
 
     @Composable
+    @OptIn(ExperimentalComposeUiApi::class)
     internal fun Content(modifier: Modifier) {
         Box(
             modifier
                 // Handle horizontal and vertical swipes on this layout.
                 // Note: order here is important and will give a slight priority to the vertical
                 // swipes.
-                .swipeToScene(gestureHandler(Orientation.Horizontal))
-                .swipeToScene(gestureHandler(Orientation.Vertical))
-                .onSizeChanged { size = it }
+                .swipeToScene(horizontalGestureHandler)
+                .swipeToScene(verticalGestureHandler)
+                // Animate the size of this layout.
+                .intermediateLayout { measurable, constraints ->
+                    // Measure content normally.
+                    val placeable = measurable.measure(constraints)
+
+                    val width: Int
+                    val height: Int
+                    val state = state.transitionState
+                    if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
+                        width = placeable.width
+                        height = placeable.height
+                    } else {
+                        // Interpolate the size.
+                        val fromSize = scene(state.fromScene).targetSize
+                        val toSize = scene(state.toScene).targetSize
+
+                        // Optimization: make sure we don't read state.progress if fromSize ==
+                        // toSize to avoid running this code every frame when the layout size does
+                        // not change.
+                        if (fromSize == toSize) {
+                            width = fromSize.width
+                            height = fromSize.height
+                        } else {
+                            val size = lerp(fromSize, toSize, state.progress)
+                            width = size.width.coerceAtLeast(0)
+                            height = size.height.coerceAtLeast(0)
+                        }
+                    }
+
+                    layout(width, height) { placeable.place(0, 0) }
+                }
         ) {
             LookaheadScope {
                 val scenesToCompose =
@@ -230,4 +257,9 @@
     }
 
     internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene)
+
+    @VisibleForTesting
+    fun setScenesTargetSizeForTest(size: IntSize) {
+        scenes.values.forEach { it.targetSize = size }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 840800d..70534dd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -38,7 +38,7 @@
         transition: TransitionState.Transition,
         value: Offset
     ): Offset {
-        val sceneSize = scene.size
+        val sceneSize = scene.targetSize
         val elementSize = sceneValues.targetSize
         if (elementSize == Element.SizeUnspecified) {
             return value
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 1e3d011..7ab2096 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -46,6 +46,7 @@
 import org.junit.runner.RunWith
 
 private const val SCREEN_SIZE = 100f
+private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())
 
 @RunWith(AndroidJUnit4::class)
 class SceneGestureHandlerTest {
@@ -80,7 +81,7 @@
                             edgeDetector = DefaultEdgeDetector,
                             coroutineScope = coroutineScope,
                         )
-                        .also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
+                        .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },
                 orientation = Orientation.Vertical,
                 coroutineScope = coroutineScope,
             )
@@ -128,18 +129,21 @@
         runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
     }
 
+    private fun DraggableHandler.onDragStarted() =
+        onDragStarted(layoutSize = LAYOUT_SIZE, startedPosition = Offset.Zero)
+
     @Test
     fun testPreconditions() = runGestureTest { assertScene(currentScene = SceneA, isIdle = true) }
 
     @Test
     fun onDragStarted_shouldStartATransition() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
     }
 
     @Test
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
@@ -152,7 +156,7 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -170,7 +174,7 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -188,7 +192,7 @@
 
     @Test
     fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDragStopped(velocity = 0f)
@@ -197,7 +201,7 @@
 
     @Test
     fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -217,7 +221,7 @@
         assertScene(currentScene = SceneC, isIdle = false)
 
         // Start a new gesture while the offset is animating
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
     }
 
@@ -421,6 +425,7 @@
         draggable.onDelta(deltaInPixels10)
         assertScene(currentScene = SceneA, isIdle = true)
     }
+
     @Test
     fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
         draggable.onDragStopped(velocityThreshold)
@@ -437,7 +442,7 @@
     @Test
     fun startNestedScrollWhileDragging() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
-        draggable.onDragStarted(Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 5afd420..321cf63 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -18,6 +18,8 @@
 
 import androidx.activity.ComponentActivity
 import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -48,6 +50,7 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.assertSizeIsEqualTo
 import com.android.compose.test.subjects.DpOffsetSubject
 import com.android.compose.test.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
@@ -307,6 +310,26 @@
         assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
     }
 
+    @Test
+    fun layoutSizeIsAnimated() {
+        val layoutTag = "layout"
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.size(200.dp, 100.dp)) },
+            toSceneContent = { Box(Modifier.size(120.dp, 140.dp)) },
+            transition = {
+                // 4 frames of animation.
+                spec = tween(4 * 16, easing = LinearEasing)
+            },
+            layoutModifier = Modifier.testTag(layoutTag),
+        ) {
+            before { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(200.dp, 100.dp) }
+            at(16) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(180.dp, 110.dp) }
+            at(32) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(160.dp, 120.dp) }
+            at(48) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(140.dp, 130.dp) }
+            after { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(120.dp, 140.dp) }
+        }
+    }
+
     private fun SemanticsNodeInteraction.offsetRelativeTo(
         other: SemanticsNodeInteraction,
     ): DpOffset {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
index 2a27763..8cffcf6 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
@@ -48,7 +48,7 @@
         rule.testTransition(
             // The layout under test is 300dp x 300dp.
             layoutModifier = Modifier.size(300.dp),
-            fromSceneContent = {},
+            fromSceneContent = { Box(Modifier.fillMaxSize()) },
             toSceneContent = {
                 // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp)
                 Box(Modifier.fillMaxSize()) {
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index e0ae1be..06de296 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -16,7 +16,6 @@
 
 package com.android.compose.animation.scene
 
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -104,7 +103,7 @@
                 currentScene,
                 onChangeScene,
                 transitions { from(fromScene, to = toScene, transition) },
-                layoutModifier.fillMaxSize(),
+                layoutModifier,
             ) {
                 scene(fromScene, content = fromSceneContent)
                 scene(toScene, content = toSceneContent)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
index 15d4d20..67ce86b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
@@ -25,7 +25,7 @@
 import com.android.internal.logging.testing.FakeMetricsLogger
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags.REFACTOR_GETCURRENTUSER
 import com.android.systemui.log.table.TableLogBuffer
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8780f58..9a3c6d5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3266,4 +3266,9 @@
     <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
     <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
     <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+
+    <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
+    <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
+    <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
+    <string name="keyboard_backlight_value">Level %1$d of %2$d</string>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index f091558..07359d1 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -60,10 +60,10 @@
 import com.android.systemui.biometrics.AuthRippleController;
 import com.android.systemui.biometrics.UdfpsController;
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
-import com.android.systemui.bouncer.domain.interactor.BouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -127,7 +127,7 @@
     @NonNull private final KeyguardTransitionInteractor mTransitionInteractor;
     @NonNull private final KeyguardInteractor mKeyguardInteractor;
     @NonNull private final View.AccessibilityDelegate mAccessibilityDelegate;
-    @NonNull private final Lazy<BouncerInteractor> mBouncerInteractor;
+    @NonNull private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractor;
     @NonNull private final SceneContainerFlags mSceneContainerFlags;
 
     // Tracks the velocity of a touch to help filter out the touches that move too fast.
@@ -205,7 +205,7 @@
             @NonNull FeatureFlags featureFlags,
             PrimaryBouncerInteractor primaryBouncerInteractor,
             Context context,
-            Lazy<BouncerInteractor> bouncerInteractor,
+            Lazy<DeviceEntryInteractor> deviceEntryInteractor,
             SceneContainerFlags sceneContainerFlags
     ) {
         mStatusBarStateController = statusBarStateController;
@@ -232,7 +232,7 @@
         dumpManager.registerDumpable(TAG, this);
         mResources = resources;
         mContext = context;
-        mBouncerInteractor = bouncerInteractor;
+        mDeviceEntryInteractor = deviceEntryInteractor;
         mSceneContainerFlags = sceneContainerFlags;
 
         mAccessibilityDelegate = new View.AccessibilityDelegate() {
@@ -747,7 +747,7 @@
         vibrateOnLongPress();
 
         if (mSceneContainerFlags.isEnabled()) {
-            mBouncerInteractor.get().showOrUnlockDevice(null);
+            mDeviceEntryInteractor.get().attemptDeviceEntry();
         } else {
             mKeyguardViewController.showPrimaryBouncer(/* scrim */ true);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 80be008..ee3a55f 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -25,7 +25,7 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockscreenCredential
 import com.android.keyguard.KeyguardSecurityModel
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationResultModel
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -45,7 +45,9 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
@@ -65,7 +67,13 @@
      * Note that the length of the PIN is also important to take into consideration, please see
      * [hintedPinLength].
      */
-    val isAutoConfirmEnabled: StateFlow<Boolean>
+    val isAutoConfirmFeatureEnabled: StateFlow<Boolean>
+
+    /**
+     * Emits the result whenever a PIN/Pattern/Password security challenge is attempted by the user
+     * in order to unlock the device.
+     */
+    val authenticationChallengeResult: SharedFlow<Boolean>
 
     /**
      * The exact length a PIN should be for us to enable PIN length hinting.
@@ -152,18 +160,19 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
     private val userRepository: UserRepository,
     private val lockPatternUtils: LockPatternUtils,
     broadcastDispatcher: BroadcastDispatcher,
 ) : AuthenticationRepository {
 
-    override val isAutoConfirmEnabled: StateFlow<Boolean> =
+    override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
         refreshingFlow(
             initialValue = false,
             getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled,
         )
+    override val authenticationChallengeResult = MutableSharedFlow<Boolean>()
 
     override val hintedPinLength: Int = 6
 
@@ -224,6 +233,7 @@
             } else {
                 lockPatternUtils.reportFailedPasswordAttempt(selectedUserId)
             }
+            authenticationChallengeResult.emit(isSuccessful)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 345f15c..1ede530 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,19 +16,16 @@
 
 package com.android.systemui.authentication.domain.interactor
 
-import com.android.app.tracing.TraceUtils.Companion.async
 import com.android.app.tracing.TraceUtils.Companion.withContext
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.AuthenticationRepository
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
@@ -40,13 +37,14 @@
 import kotlinx.coroutines.async
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 
 /**
  * Hosts application business logic related to user authentication.
@@ -63,7 +61,6 @@
     private val repository: AuthenticationRepository,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val userRepository: UserRepository,
-    private val deviceEntryRepository: DeviceEntryRepository,
     private val clock: SystemClock,
 ) {
     /**
@@ -84,8 +81,7 @@
      * `true` even when the lockscreen is showing and still needs to be dismissed by the user to
      * proceed.
      */
-    val authenticationMethod: Flow<DomainLayerAuthenticationMethodModel> =
-        repository.authenticationMethod.map { rawModel -> rawModel.toDomainLayer() }
+    val authenticationMethod: Flow<AuthenticationMethodModel> = repository.authenticationMethod
 
     /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */
     val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling
@@ -103,9 +99,29 @@
                 initialValue = throttling.value.remainingMs > 0,
             )
 
+    /**
+     * Whether the auto confirm feature is enabled for the currently-selected user.
+     *
+     * Note that the length of the PIN is also important to take into consideration, please see
+     * [hintedPinLength].
+     *
+     * During throttling, this is always disabled (`false`).
+     */
+    val isAutoConfirmEnabled: StateFlow<Boolean> =
+        combine(repository.isAutoConfirmFeatureEnabled, isThrottled) { featureEnabled, isThrottled
+                ->
+                // Disable auto-confirm during throttling.
+                featureEnabled && !isThrottled
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
+
     /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */
     val hintedPinLength: StateFlow<Int?> =
-        repository.isAutoConfirmEnabled
+        isAutoConfirmEnabled
             .map { isAutoConfirmEnabled ->
                 repository.getPinLength().takeIf {
                     isAutoConfirmEnabled && it == repository.hintedPinLength
@@ -119,12 +135,16 @@
                 initialValue = null,
             )
 
-    /** Whether the auto confirm feature is enabled for the currently-selected user. */
-    val isAutoConfirmEnabled: StateFlow<Boolean> = repository.isAutoConfirmEnabled
-
     /** Whether the pattern should be visible for the currently-selected user. */
     val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible
 
+    /**
+     * Emits the outcome (successful or unsuccessful) whenever a PIN/Pattern/Password security
+     * challenge is attempted by the user in order to unlock the device.
+     */
+    val authenticationChallengeResult: SharedFlow<Boolean> =
+        repository.authenticationChallengeResult
+
     private var throttlingCountdownJob: Job? = null
 
     init {
@@ -147,17 +167,8 @@
      * The flow should be used for code that wishes to stay up-to-date its logic as the
      * authentication changes over time and this method should be used for simple code that only
      * needs to check the current value.
-     *
-     * Note: this layer adds the synthetic authentication method of "swipe" which is special. When
-     * the current authentication method is "swipe", the user does not need to complete any
-     * authentication challenge to unlock the device; they just need to dismiss the lockscreen to
-     * get past it. This also means that the value of `DeviceEntryInteractor#isUnlocked` remains
-     * `true` even when the lockscreen is showing and still needs to be dismissed by the user to
-     * proceed.
      */
-    suspend fun getAuthenticationMethod(): DomainLayerAuthenticationMethodModel {
-        return repository.getAuthenticationMethod().toDomainLayer()
-    }
+    suspend fun getAuthenticationMethod() = repository.getAuthenticationMethod()
 
     /**
      * Attempts to authenticate the user and unlock the device.
@@ -187,13 +198,13 @@
                 // attempt.
                 isThrottled.value -> true
                 // The pattern is too short; skip the attempt.
-                authMethod == DomainLayerAuthenticationMethodModel.Pattern &&
+                authMethod == AuthenticationMethodModel.Pattern &&
                     input.size < repository.minPatternLength -> true
                 // Auto-confirm attempt when the feature is not enabled; skip the attempt.
                 tryAutoConfirm && !isAutoConfirmEnabled.value -> true
                 // Auto-confirm should skip the attempt if the pin entered is too short.
                 tryAutoConfirm &&
-                    authMethod == DomainLayerAuthenticationMethodModel.Pin &&
+                    authMethod == AuthenticationMethodModel.Pin &&
                     input.size < repository.getPinLength() -> true
                 else -> false
             }
@@ -279,15 +290,15 @@
         }
     }
 
-    private fun DomainLayerAuthenticationMethodModel.createCredential(
+    private fun AuthenticationMethodModel.createCredential(
         input: List<Any>
     ): LockscreenCredential? {
         return when (this) {
-            is DomainLayerAuthenticationMethodModel.Pin ->
+            is AuthenticationMethodModel.Pin ->
                 LockscreenCredential.createPin(input.joinToString(""))
-            is DomainLayerAuthenticationMethodModel.Password ->
+            is AuthenticationMethodModel.Password ->
                 LockscreenCredential.createPassword(input.joinToString(""))
-            is DomainLayerAuthenticationMethodModel.Pattern ->
+            is AuthenticationMethodModel.Pattern ->
                 LockscreenCredential.createPattern(
                     input
                         .map { it as AuthenticationPatternCoordinate }
@@ -297,23 +308,6 @@
         }
     }
 
-    private suspend fun DataLayerAuthenticationMethodModel.toDomainLayer():
-        DomainLayerAuthenticationMethodModel {
-        return when (this) {
-            is DataLayerAuthenticationMethodModel.None ->
-                if (deviceEntryRepository.isInsecureLockscreenEnabled()) {
-                    DomainLayerAuthenticationMethodModel.Swipe
-                } else {
-                    DomainLayerAuthenticationMethodModel.None
-                }
-            is DataLayerAuthenticationMethodModel.Pin -> DomainLayerAuthenticationMethodModel.Pin
-            is DataLayerAuthenticationMethodModel.Password ->
-                DomainLayerAuthenticationMethodModel.Password
-            is DataLayerAuthenticationMethodModel.Pattern ->
-                DomainLayerAuthenticationMethodModel.Pattern
-        }
-    }
-
     companion object {
         const val TAG = "AuthenticationInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/model/AuthenticationMethodModel.kt
deleted file mode 100644
index d7e6099..0000000
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/model/AuthenticationMethodModel.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.authentication.domain.model
-
-/** Enumerates all known authentication methods. */
-sealed class AuthenticationMethodModel(
-    /**
-     * Whether the authentication method is considered to be "secure".
-     *
-     * "Secure" authentication methods require authentication to unlock the device. Non-secure auth
-     * methods simply require user dismissal.
-     */
-    open val isSecure: Boolean,
-) {
-    /** There is no authentication method on the device. We shouldn't even show the lock screen. */
-    object None : AuthenticationMethodModel(isSecure = false)
-
-    /** The most basic authentication method. The lock screen can be swiped away when displayed. */
-    object Swipe : AuthenticationMethodModel(isSecure = false)
-
-    object Pin : AuthenticationMethodModel(isSecure = true)
-
-    object Password : AuthenticationMethodModel(isSecure = true)
-
-    object Pattern : AuthenticationMethodModel(isSecure = true)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/authentication/data/model/AuthenticationMethodModel.kt
rename to packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
index 6d23b11..bb5b81d 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/model/AuthenticationMethodModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.authentication.data.model
+package com.android.systemui.authentication.shared.model
 
 /** Enumerates all known authentication methods. */
 sealed class AuthenticationMethodModel(
@@ -26,7 +26,10 @@
      */
     open val isSecure: Boolean,
 ) {
-    /** There is no authentication method on the device. We shouldn't even show the lock screen. */
+    /**
+     * Device doesn't use a secure authentication method. Either there is no lockscreen or the lock
+     * screen can be swiped away when displayed.
+     */
     object None : AuthenticationMethodModel(isSecure = false)
 
     object Pin : AuthenticationMethodModel(isSecure = true)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index b9a913e..4e1cddc 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -19,24 +19,22 @@
 import android.content.Context
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.classifier.FalsingClassifier
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.scene.shared.model.SceneKey
-import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.util.kotlin.pairwise
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
@@ -51,9 +49,7 @@
     @Application private val applicationScope: CoroutineScope,
     @Application private val applicationContext: Context,
     private val repository: BouncerRepository,
-    private val deviceEntryInteractor: DeviceEntryInteractor,
     private val authenticationInteractor: AuthenticationInteractor,
-    private val sceneInteractor: SceneInteractor,
     flags: SceneContainerFlags,
     private val falsingInteractor: FalsingInteractor,
 ) {
@@ -100,6 +96,10 @@
     val isUserSwitcherVisible: Boolean
         get() = repository.isUserSwitcherVisible
 
+    private val _onImeHidden = MutableSharedFlow<Unit>()
+    /** Provide the onImeHidden events from the bouncer */
+    val onImeHidden: SharedFlow<Unit> = _onImeHidden
+
     init {
         if (flags.isEnabled()) {
             // Clear the message if moved from throttling to no-longer throttling.
@@ -142,32 +142,6 @@
     }
 
     /**
-     * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
-     *
-     * @param message An optional message to show to the user in the bouncer.
-     */
-    fun showOrUnlockDevice(
-        message: String? = null,
-    ) {
-        applicationScope.launch {
-            if (deviceEntryInteractor.isAuthenticationRequired()) {
-                repository.setMessage(
-                    message ?: promptMessage(authenticationInteractor.getAuthenticationMethod())
-                )
-                sceneInteractor.changeScene(
-                    scene = SceneModel(SceneKey.Bouncer),
-                    loggingReason = "request to unlock device while authentication required",
-                )
-            } else {
-                sceneInteractor.changeScene(
-                    scene = SceneModel(SceneKey.Gone),
-                    loggingReason = "request to unlock device while authentication isn't required",
-                )
-            }
-        }
-    }
-
-    /**
      * Resets the user-facing message back to the default according to the current authentication
      * method.
      */
@@ -212,17 +186,11 @@
         return applicationScope
             .async {
                 val authResult = authenticationInteractor.authenticate(input, tryAutoConfirm)
-                when (authResult) {
-                    // Authentication succeeded.
-                    AuthenticationResult.SUCCEEDED ->
-                        sceneInteractor.changeScene(
-                            scene = SceneModel(SceneKey.Gone),
-                            loggingReason = "successful authentication",
-                        )
-                    // Authentication failed.
-                    AuthenticationResult.FAILED -> showErrorMessage()
-                    // Authentication skipped.
-                    AuthenticationResult.SKIPPED -> if (!tryAutoConfirm) showErrorMessage()
+                if (
+                    authResult == AuthenticationResult.FAILED ||
+                        (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
+                ) {
+                    showErrorMessage()
                 }
                 authResult
             }
@@ -242,16 +210,8 @@
     }
 
     /** Notifies the interactor that the input method editor has been hidden. */
-    fun onImeHidden() {
-        // If the bouncer is showing, hide it and return to the lockscreen scene.
-        if (sceneInteractor.desiredScene.value.key != SceneKey.Bouncer) {
-            return
-        }
-
-        sceneInteractor.changeScene(
-            scene = SceneModel(SceneKey.Lockscreen),
-            loggingReason = "IME hidden",
-        )
+    suspend fun onImeHidden() {
+        _onImeHidden.emit(Unit)
     }
 
     private fun promptMessage(authMethod: AuthenticationMethodModel): String {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 55bc653..f46574c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -18,7 +18,7 @@
 
 import android.annotation.StringRes
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -75,7 +75,7 @@
      * Notifies that the input method editor (for example, the software keyboard) has been shown or
      * hidden.
      */
-    fun onImeVisibilityChanged(isVisible: Boolean) {
+    suspend fun onImeVisibilityChanged(isVisible: Boolean) {
         if (isImeVisible && !isVisible) {
             interactor.onImeHidden()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 73d15f0..09c94c8 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -20,7 +20,7 @@
 import android.graphics.Bitmap
 import androidx.core.graphics.drawable.toBitmap
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
@@ -180,6 +180,19 @@
                 initialValue = isSideBySideSupported(authMethodViewModel.value),
             )
 
+    /**
+     * Whether the splitting the UI around the fold seam (where the hinge is on a foldable device)
+     * is required.
+     */
+    val isFoldSplitRequired: StateFlow<Boolean> =
+        authMethodViewModel
+            .map { authMethod -> isFoldSplitRequired(authMethod) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = isFoldSplitRequired(authMethodViewModel.value),
+            )
+
     init {
         if (flags.isEnabled()) {
             applicationScope.launch {
@@ -212,6 +225,10 @@
         return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
     }
 
+    private fun isFoldSplitRequired(authMethod: AuthMethodBouncerViewModel?): Boolean {
+        return authMethod !is PasswordBouncerViewModel
+    }
+
     private fun toMessageViewModel(
         message: String?,
         isThrottled: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index fe77419..a15698e 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.res.R
 import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index d301085..ed6a48f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -18,7 +18,7 @@
 
 import android.content.Context
 import android.util.TypedValue
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.res.R
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index b90e255..2ed0d5d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -18,7 +18,7 @@
 
 import android.content.Context
 import com.android.keyguard.PinShapeAdapter
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.res.R
 import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 6b27ce0..f7fee96 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -18,13 +18,11 @@
 
 import android.appwidget.AppWidgetHost
 import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProviderInfo
 import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
-import android.content.pm.PackageManager
 import android.os.UserManager
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -32,13 +30,10 @@
 import com.android.systemui.communal.data.db.CommunalWidgetDao
 import com.android.systemui.communal.data.db.CommunalWidgetItem
 import com.android.systemui.communal.shared.CommunalWidgetHost
-import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
@@ -58,9 +53,6 @@
 
 /** Encapsulates the state of widgets for communal mode. */
 interface CommunalWidgetRepository {
-    /** A flow of provider info for the stopwatch widget, or null if widget is unavailable. */
-    val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?>
-
     /** A flow of information about active communal widgets stored in database. */
     val communalWidgets: Flow<List<CommunalWidgetContentModel>>
 
@@ -84,15 +76,12 @@
     communalRepository: CommunalRepository,
     private val communalWidgetHost: CommunalWidgetHost,
     private val communalWidgetDao: CommunalWidgetDao,
-    private val packageManager: PackageManager,
     private val userManager: UserManager,
     private val userTracker: UserTracker,
     @CommunalLog logBuffer: LogBuffer,
-    featureFlags: FeatureFlagsClassic,
 ) : CommunalWidgetRepository {
     companion object {
         const val TAG = "CommunalWidgetRepository"
-        const val WIDGET_LABEL = "Stopwatch"
     }
 
     private val logger = Logger(logBuffer, TAG)
@@ -100,9 +89,6 @@
     // Whether the [AppWidgetHost] is listening for updates.
     private var isHostListening = false
 
-    // Widgets that should be rendered in communal mode.
-    private val widgets: HashMap<Int, CommunalAppWidgetInfo> = hashMapOf()
-
     private val isUserUnlocked: Flow<Boolean> =
         callbackFlow {
                 if (!communalRepository.isCommunalEnabled) {
@@ -149,25 +135,6 @@
             }
         }
 
-    override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> =
-        isHostActive.map { isHostActive ->
-            if (!isHostActive || !featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)) {
-                return@map null
-            }
-
-            val providerInfo =
-                appWidgetManager.installedProviders.find {
-                    it.loadLabel(packageManager).equals(WIDGET_LABEL)
-                }
-
-            if (providerInfo == null) {
-                logger.w("Cannot find app widget: $WIDGET_LABEL")
-                return@map null
-            }
-
-            return@map addStopWatchWidget(providerInfo)
-        }
-
     override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
         isHostActive.flatMapLatest { isHostActive ->
             if (!isHostActive) {
@@ -226,21 +193,4 @@
         appWidgetHost.stopListening()
         isHostListening = false
     }
-
-    // TODO(b/306471933): remove this prototype that shows a stopwatch in the communal blueprint
-    private fun addStopWatchWidget(providerInfo: AppWidgetProviderInfo): CommunalAppWidgetInfo {
-        val existing = widgets.values.firstOrNull { it.providerInfo == providerInfo }
-        if (existing != null) {
-            return existing
-        }
-
-        val appWidgetId = appWidgetHost.allocateAppWidgetId()
-        val widget =
-            CommunalAppWidgetInfo(
-                providerInfo,
-                appWidgetId,
-            )
-        widgets[appWidgetId] = widget
-        return widget
-    }
 }
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 eb36b19..508f52c 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,7 +23,6 @@
 import com.android.systemui.communal.data.repository.CommunalRepository
 import com.android.systemui.communal.data.repository.CommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.dagger.SysUISingleton
@@ -55,9 +54,6 @@
     val isCommunalEnabled: Boolean
         get() = communalRepository.isCommunalEnabled
 
-    /** A flow of info about the widget to be displayed, or null if widget is unavailable. */
-    val appWidgetInfo: Flow<CommunalAppWidgetInfo?> = widgetRepository.stopwatchAppWidgetInfo
-
     /**
      * Target scene as requested by the underlying [SceneTransitionLayout] or through
      * [onSceneChanged].
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalWidgetViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalWidgetViewBinder.kt
deleted file mode 100644
index 65bf4b3..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalWidgetViewBinder.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.communal.ui.binder
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.res.R
-import com.android.systemui.communal.ui.adapter.CommunalWidgetViewAdapter
-import com.android.systemui.communal.ui.view.CommunalWidgetWrapper
-import com.android.systemui.communal.ui.viewmodel.CommunalWidgetViewModel
-import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
-import com.android.systemui.keyguard.ui.view.KeyguardRootView
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.launch
-
-/** Binds [CommunalWidgetViewModel] to the keyguard root view. */
-object CommunalWidgetViewBinder {
-
-    @JvmStatic
-    fun bind(
-        rootView: KeyguardRootView,
-        viewModel: CommunalWidgetViewModel,
-        adapter: CommunalWidgetViewAdapter,
-        keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
-    ) {
-        rootView.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    adapter.adapt(viewModel.appWidgetInfo).collect {
-                        val oldView =
-                            rootView.findViewById<CommunalWidgetWrapper>(
-                                R.id.communal_widget_wrapper
-                            )
-                        var dirty = false
-
-                        if (oldView != null) {
-                            rootView.removeView(oldView)
-                            dirty = true
-                        }
-
-                        if (it != null) {
-                            rootView.addView(it)
-                            dirty = true
-                        }
-
-                        if (dirty) {
-                            keyguardBlueprintInteractor.refreshBlueprint()
-                        }
-                    }
-                }
-
-                launch { viewModel.alpha.collect { rootView.alpha = it } }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt
index 702554a..9198c7b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.communal.ui.view.layout.blueprints
 
 import com.android.systemui.communal.ui.view.layout.sections.DefaultCommunalHubSection
-import com.android.systemui.communal.ui.view.layout.sections.DefaultCommunalWidgetSection
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -30,13 +29,11 @@
 @Inject
 constructor(
     defaultCommunalHubSection: DefaultCommunalHubSection,
-    defaultCommunalWidgetSection: DefaultCommunalWidgetSection,
 ) : KeyguardBlueprint {
     override val id: String = COMMUNAL
     override val sections: List<KeyguardSection> =
         listOf(
             defaultCommunalHubSection,
-            defaultCommunalWidgetSection,
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/DefaultCommunalWidgetSection.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/DefaultCommunalWidgetSection.kt
deleted file mode 100644
index c3e8a96..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/DefaultCommunalWidgetSection.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.communal.ui.view.layout.sections
-
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import com.android.systemui.res.R
-import com.android.systemui.communal.ui.adapter.CommunalWidgetViewAdapter
-import com.android.systemui.communal.ui.binder.CommunalWidgetViewBinder
-import com.android.systemui.communal.ui.viewmodel.CommunalWidgetViewModel
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.view.KeyguardRootView
-import dagger.Lazy
-import javax.inject.Inject
-
-class DefaultCommunalWidgetSection
-@Inject
-constructor(
-    private val featureFlags: FeatureFlags,
-    private val keyguardRootView: KeyguardRootView,
-    private val communalWidgetViewModel: CommunalWidgetViewModel,
-    private val communalWidgetViewAdapter: CommunalWidgetViewAdapter,
-    private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
-) : KeyguardSection() {
-    private val widgetAreaViewId = R.id.communal_widget_wrapper
-
-    override fun addViews(constraintLayout: ConstraintLayout) {}
-
-    override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)) {
-            return
-        }
-
-        CommunalWidgetViewBinder.bind(
-            keyguardRootView,
-            communalWidgetViewModel,
-            communalWidgetViewAdapter,
-            keyguardBlueprintInteractor.get(),
-        )
-    }
-
-    override fun applyConstraints(constraintSet: ConstraintSet) {
-        constraintSet.apply {
-            constrainWidth(widgetAreaViewId, WRAP_CONTENT)
-            constrainHeight(widgetAreaViewId, WRAP_CONTENT)
-            connect(widgetAreaViewId, BOTTOM, PARENT_ID, BOTTOM)
-            connect(widgetAreaViewId, END, PARENT_ID, END)
-        }
-    }
-
-    override fun removeViews(constraintLayout: ConstraintLayout) {}
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt
deleted file mode 100644
index d7bbea6..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.communal.ui.viewmodel
-
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-
-@SysUISingleton
-class CommunalWidgetViewModel
-@Inject
-constructor(
-    communalInteractor: CommunalInteractor,
-    keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
-) {
-    /** An observable for the alpha level for the communal widget area. */
-    val alpha: Flow<Float> = keyguardBottomAreaViewModel.alpha
-
-    /** A flow of info about the widget to be displayed, or null if widget is unavailable. */
-    val appWidgetInfo: Flow<CommunalAppWidgetInfo?> = communalInteractor.appWidgetInfo
-}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
index 1e29e1f..f27bbe6 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.deviceentry.data.repository
 
+import android.util.Log
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -15,9 +16,12 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
 
@@ -35,10 +39,14 @@
     val isUnlocked: StateFlow<Boolean>
 
     /**
-     * Whether the lockscreen should be shown when the authentication method is not secure (e.g.
-     * `None` or `Swipe`).
+     * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has
+     * chosen any secure authentication method and even if they set the lockscreen to be dismissed
+     * when the user swipes on it.
      */
-    suspend fun isInsecureLockscreenEnabled(): Boolean
+    suspend fun isLockscreenEnabled(): Boolean
+
+    /** Report successful authentication for device entry. */
+    fun reportSuccessfulAuthentication()
 
     /**
      * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
@@ -67,7 +75,9 @@
     keyguardStateController: KeyguardStateController,
 ) : DeviceEntryRepository {
 
-    override val isUnlocked =
+    private val _isUnlocked = MutableStateFlow(false)
+
+    private val isUnlockedReportedByLegacyKeyguard =
         conflatedCallbackFlow {
                 val callback =
                     object : KeyguardStateController.Callback {
@@ -99,19 +109,27 @@
                 awaitClose { keyguardStateController.removeCallback(callback) }
             }
             .distinctUntilChanged()
+            .onEach { _isUnlocked.value = it }
             .stateIn(
                 applicationScope,
                 SharingStarted.Eagerly,
                 initialValue = false,
             )
 
-    override suspend fun isInsecureLockscreenEnabled(): Boolean {
+    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
+
+    override suspend fun isLockscreenEnabled(): Boolean {
         return withContext(backgroundDispatcher) {
             val selectedUserId = userRepository.getSelectedUserInfo().id
             !lockPatternUtils.isLockScreenDisabled(selectedUserId)
         }
     }
 
+    override fun reportSuccessfulAuthentication() {
+        Log.d(TAG, "Successful authentication reported.")
+        _isUnlocked.value = true
+    }
+
     override val isBypassEnabled: StateFlow<Boolean> =
         conflatedCallbackFlow {
                 val listener =
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index e872d13..c3f3529 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -17,23 +17,28 @@
 package com.android.systemui.deviceentry.domain.interactor
 
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.TrustRepository
 import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /**
  * Hosts application business logic related to device entry.
@@ -48,9 +53,10 @@
     @Application private val applicationScope: CoroutineScope,
     repository: DeviceEntryRepository,
     private val authenticationInteractor: AuthenticationInteractor,
-    sceneInteractor: SceneInteractor,
+    private val sceneInteractor: SceneInteractor,
     deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
     trustRepository: TrustRepository,
+    flags: SceneContainerFlags,
 ) {
     /**
      * Whether the device is unlocked.
@@ -90,28 +96,33 @@
             .map { it == SceneKey.Gone }
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
+                started = SharingStarted.Eagerly,
                 initialValue = false,
             )
 
     // Authenticated by a TrustAgent like trusted device, location, etc or by face auth.
     private val passivelyAuthenticated =
         merge(
-            trustRepository.isCurrentUserTrusted,
-            deviceEntryFaceAuthRepository.isAuthenticated,
-        )
+                trustRepository.isCurrentUserTrusted,
+                deviceEntryFaceAuthRepository.isAuthenticated,
+            )
+            .onStart { emit(false) }
 
     /**
      * Whether it's currently possible to swipe up to enter the device without requiring
-     * authentication. This returns `false` whenever the lockscreen has been dismissed.
+     * authentication or when the device is already authenticated using a passive authentication
+     * mechanism like face or trust manager. This returns `false` whenever the lockscreen has been
+     * dismissed.
      *
      * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other
      * UI.
      */
     val canSwipeToEnter =
         combine(
+                // This is true when the user has chosen to show the lockscreen but has not made it
+                // secure.
                 authenticationInteractor.authenticationMethod.map {
-                    it == AuthenticationMethodModel.Swipe
+                    it == AuthenticationMethodModel.None && repository.isLockscreenEnabled()
                 },
                 passivelyAuthenticated,
                 isDeviceEntered
@@ -120,11 +131,37 @@
             }
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
+                started = SharingStarted.Eagerly,
                 initialValue = false,
             )
 
     /**
+     * Attempt to enter the device and dismiss the lockscreen. If authentication is required to
+     * unlock the device it will transition to bouncer.
+     */
+    fun attemptDeviceEntry() {
+        // TODO (b/307768356),
+        //       1. Check if the device is already authenticated by trust agent/passive biometrics
+        //       2. show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show
+        //       3. For face auth only setups trigger face auth, delay transitioning to bouncer for
+        //          a small amount of time.
+        //       4. Transition to bouncer scene
+        applicationScope.launch {
+            if (isAuthenticationRequired()) {
+                sceneInteractor.changeScene(
+                    scene = SceneModel(SceneKey.Bouncer),
+                    loggingReason = "request to unlock device while authentication required",
+                )
+            } else {
+                sceneInteractor.changeScene(
+                    scene = SceneModel(SceneKey.Gone),
+                    loggingReason = "request to unlock device while authentication isn't required",
+                )
+            }
+        }
+    }
+
+    /**
      * Returns `true` if the device currently requires authentication before entry is granted;
      * `false` if the device can be entered without authenticating first.
      */
@@ -133,10 +170,22 @@
     }
 
     /**
-     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+     * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
      * dismissed once the authentication challenge is completed. For example, completing a biometric
      * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
-     * lock screen.
+     * lockscreen.
      */
     val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+
+    init {
+        if (flags.isEnabled()) {
+            applicationScope.launch {
+                authenticationInteractor.authenticationChallengeResult.collectLatest { successful ->
+                    if (successful) {
+                        repository.reportSuccessfulAuthentication()
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 737d62d..ced96f1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -239,8 +239,7 @@
 
     /** Provide new auth messages on the bouncer. */
     // TODO(b/277961132): Tracking bug.
-    @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag("revamped_bouncer_messages",
-            teamfood = true)
+    @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag("revamped_bouncer_messages")
 
     /** Keyguard Migration */
 
@@ -255,9 +254,6 @@
     // TODO(b/287268101): Tracking bug.
     @JvmField val TRANSIT_CLOCK = releasedFlag("lockscreen_custom_transit_clock")
 
-    // TODO(b/288276738): Tracking bug.
-    @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag("widget_on_keyguard")
-
     /** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
     // TODO(b/288074305): Tracking bug.
     @JvmField val MIGRATE_NSSL = unreleasedFlag("migrate_nssl")
@@ -773,9 +769,4 @@
     @JvmField
     val COMMUNAL_SERVICE_ENABLED = resourceBooleanFlag(R.bool.config_communalServiceEnabled,
         "communal_service_enabled")
-
-    // TODO(b/303131306): Tracking Bug
-    /** Whether communal hub features are enabled. */
-    @JvmField
-    val COMMUNAL_HUB = unreleasedFlag("communal_hub")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index e16bb0b..1e9be09 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -30,6 +30,7 @@
 import android.view.ViewGroup.MarginLayoutParams
 import android.view.Window
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
@@ -78,23 +79,29 @@
     private lateinit var stepProperties: StepViewProperties
 
     @ColorInt
-    var filledRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
-    @ColorInt
-    var emptyRectangleColor =
-        getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
-    @ColorInt
-    var backgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
-    @ColorInt
-    var defaultIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
-    @ColorInt
-    var defaultIconBackgroundColor =
+    private val filledRectangleColor =
         getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
     @ColorInt
-    var dimmedIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+    private val emptyRectangleColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
     @ColorInt
-    var dimmedIconBackgroundColor =
+    private val backgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
+    @ColorInt
+    private val defaultIconColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+    @ColorInt
+    private val defaultIconBackgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
+    @ColorInt
+    private val dimmedIconColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+    @ColorInt
+    private val dimmedIconBackgroundColor =
         getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim)
 
+    private val levelContentDescription = context.getString(R.string.keyboard_backlight_value)
+
     init {
         currentLevel = initialCurrentLevel
         maxLevel = initialMaxLevel
@@ -103,6 +110,8 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         setUpWindowProperties(this)
         setWindowPosition()
+        // title is used for a11y announcement
+        window?.setTitle(context.getString(R.string.keyboard_backlight_dialog_title))
         updateResources()
         rootView = buildRootView()
         setContentView(rootView)
@@ -159,6 +168,12 @@
         currentLevel = current
         updateIconTile()
         updateStepColors()
+        updateAccessibilityInfo()
+    }
+
+    private fun updateAccessibilityInfo() {
+        rootView.contentDescription = String.format(levelContentDescription, currentLevel, maxLevel)
+        rootView.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
     }
 
     private fun updateIconTile() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 078feff..c2aedca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -44,6 +45,7 @@
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     controller: NotificationStackScrollLayoutController,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) :
     NotificationStackScrollLayoutSection(
@@ -53,6 +55,7 @@
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         controller,
+        notificationStackSizeCalculator,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 00966f2..ea2bdf7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
@@ -40,6 +41,7 @@
     private val sharedNotificationContainer: SharedNotificationContainer,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     private val controller: NotificationStackScrollLayoutController,
+    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
 ) : KeyguardSection() {
     private val placeHolderId = R.id.nssl_placeholder
     private var disposableHandle: DisposableHandle? = null
@@ -69,6 +71,7 @@
                 sharedNotificationContainer,
                 sharedNotificationContainerViewModel,
                 controller,
+                notificationStackSizeCalculator,
             )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index bf95c77..dc2ad8d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -44,6 +45,7 @@
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     controller: NotificationStackScrollLayoutController,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) :
     NotificationStackScrollLayoutSection(
@@ -53,6 +55,7 @@
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         controller,
+        notificationStackSizeCalculator,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 1ec43c5..d277f32 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -60,7 +60,7 @@
     }
 
     companion object {
-        @JvmField val GUTS_ANIMATION_DURATION = 500L
+        @JvmField val GUTS_ANIMATION_DURATION = 234L
     }
 
     /** A listener when the current dimensions of the player change */
@@ -234,7 +234,8 @@
             currentStartLocation,
             currentEndLocation,
             currentTransitionProgress,
-            applyImmediately = false
+            applyImmediately = false,
+            isGutsAnimation = true,
         )
     }
 
@@ -254,7 +255,8 @@
             currentStartLocation,
             currentEndLocation,
             currentTransitionProgress,
-            applyImmediately = immediate
+            applyImmediately = immediate,
+            isGutsAnimation = true,
         )
     }
 
@@ -414,7 +416,10 @@
      * it's not available, it will recreate one by measuring, which may be expensive.
      */
     @VisibleForTesting
-    fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+    fun obtainViewState(
+        state: MediaHostState?,
+        isGutsAnimation: Boolean = false
+    ): TransitionViewState? {
         if (state == null || state.measurementInput == null) {
             return null
         }
@@ -423,7 +428,7 @@
         val viewState = viewStates[cacheKey]
         if (viewState != null) {
             // we already have cached this measurement, let's continue
-            if (state.squishFraction <= 1f) {
+            if (state.squishFraction <= 1f && !isGutsAnimation) {
                 return squishViewState(viewState, state.squishFraction)
             }
             return viewState
@@ -455,13 +460,14 @@
 
             // Given that we have a measurement and a view, let's get (guaranteed) viewstates
             // from the start and end state and interpolate them
-            val startViewState = obtainViewState(startState) as TransitionViewState
+            val startViewState = obtainViewState(startState, isGutsAnimation) as TransitionViewState
             val endState = state.copy().also { it.expansion = 1.0f }
-            val endViewState = obtainViewState(endState) as TransitionViewState
+            val endViewState = obtainViewState(endState, isGutsAnimation) as TransitionViewState
             result =
                 layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
         }
-        if (state.squishFraction <= 1f) {
+        // Skip the adjustments of squish view state if UMO changes due to guts animation.
+        if (state.squishFraction <= 1f && !isGutsAnimation) {
             return squishViewState(result, state.squishFraction)
         }
         return result
@@ -521,7 +527,8 @@
         @MediaLocation startLocation: Int,
         @MediaLocation endLocation: Int,
         transitionProgress: Float,
-        applyImmediately: Boolean
+        applyImmediately: Boolean,
+        isGutsAnimation: Boolean = false,
     ) =
         traceSection("MediaViewController#setCurrentState") {
             currentEndLocation = endLocation
@@ -537,7 +544,7 @@
             // Obtain the view state that we'd want to be at the end
             // The view might not be bound yet or has never been measured and in that case will be
             // reset once the state is fully available
-            var endViewState = obtainViewState(endHostState) ?: return
+            var endViewState = obtainViewState(endHostState, isGutsAnimation) ?: return
             endViewState = updateViewStateSize(endViewState, endLocation, tmpState2)!!
             layoutController.setMeasureState(endViewState)
 
@@ -548,7 +555,7 @@
             }
 
             val result: TransitionViewState
-            var startViewState = obtainViewState(startHostState)
+            var startViewState = obtainViewState(startHostState, isGutsAnimation)
             startViewState = updateViewStateSize(startViewState, startLocation, tmpState3)
 
             if (!endHostState.visible) {
@@ -602,7 +609,8 @@
                 applyImmediately,
                 shouldAnimate,
                 animationDuration,
-                animationDelay
+                animationDelay,
+                isGutsAnimation,
             )
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
index 30b87cc..f9e0b16 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
@@ -18,7 +18,10 @@
 
 import android.content.Context
 import android.service.quicksettings.Tile
+import android.view.View
+import android.widget.Switch
 import com.android.systemui.common.shared.model.Icon
+import kotlin.reflect.KClass
 
 /**
  * Represents current a state of the tile to be displayed in on the view. Consider using
@@ -111,7 +114,7 @@
         var stateDescription: CharSequence? = null
         var sideViewIcon: SideViewIcon = SideViewIcon.None
         var enabledState: EnabledState = EnabledState.ENABLED
-        var expandedAccessibilityClassName: String? = null
+        var expandedAccessibilityClass: KClass<out View>? = Switch::class
 
         fun build(): QSTileState =
             QSTileState(
@@ -124,7 +127,7 @@
                 stateDescription,
                 sideViewIcon,
                 enabledState,
-                expandedAccessibilityClassName,
+                expandedAccessibilityClass?.qualifiedName,
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 9edd2c6..5993cf1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.qs.ui.viewmodel
 
-import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import javax.inject.Inject
 
@@ -26,11 +26,9 @@
 class QuickSettingsSceneViewModel
 @Inject
 constructor(
-    private val bouncerInteractor: BouncerInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
     val shadeHeaderViewModel: ShadeHeaderViewModel,
 ) {
     /** Notifies that some content in quick settings was clicked. */
-    fun onContentClicked() {
-        bouncerInteractor.showOrUnlockDevice()
-    }
+    fun onContentClicked() = deviceEntryInteractor.attemptDeviceEntry()
 }
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 91b4d17..ca2828b 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
@@ -19,8 +19,7 @@
 package com.android.systemui.scene.domain.startable
 
 import com.android.systemui.CoreStartable
-import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorActual
 import com.android.systemui.dagger.SysUISingleton
@@ -45,6 +44,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.emptyFlow
@@ -64,7 +64,7 @@
     @Application private val applicationScope: CoroutineScope,
     private val sceneInteractor: SceneInteractor,
     private val deviceEntryInteractor: DeviceEntryInteractor,
-    private val authenticationInteractor: AuthenticationInteractor,
+    private val bouncerInteractor: BouncerInteractor,
     private val keyguardInteractor: KeyguardInteractor,
     private val flags: SceneContainerFlags,
     private val sysUiState: SysUiState,
@@ -121,6 +121,17 @@
     /** Switches between scenes based on ever-changing application state. */
     private fun automaticallySwitchScenes() {
         applicationScope.launch {
+            // TODO (b/308001302): Move this to a bouncer specific interactor.
+            bouncerInteractor.onImeHidden.collectLatest {
+                if (sceneInteractor.desiredScene.value.key == SceneKey.Bouncer) {
+                    sceneInteractor.changeScene(
+                        scene = SceneModel(SceneKey.Lockscreen),
+                        loggingReason = "IME hidden",
+                    )
+                }
+            }
+        }
+        applicationScope.launch {
             deviceEntryInteractor.isUnlocked
                 .mapNotNull { isUnlocked ->
                     val renderedScenes =
@@ -132,41 +143,41 @@
                                     transitionState.toScene,
                                 )
                         }
+                    val isOnLockscreen = renderedScenes.contains(SceneKey.Lockscreen)
+                    val isOnBouncer = renderedScenes.contains(SceneKey.Bouncer)
+                    if (!isUnlocked) {
+                        return@mapNotNull if (isOnLockscreen || isOnBouncer) {
+                            // Already on lockscreen or bouncer, no need to change scenes.
+                            null
+                        } else {
+                            // The device locked while on a scene that's not Lockscreen or Bouncer,
+                            // go to Lockscreen.
+                            SceneKey.Lockscreen to
+                                "device locked in non-Lockscreen and non-Bouncer scene"
+                        }
+                    }
+
+                    val isBypassEnabled = deviceEntryInteractor.isBypassEnabled.value
+                    val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
                     when {
-                        isUnlocked ->
-                            when {
-                                // When the device becomes unlocked in Bouncer, go to Gone.
-                                renderedScenes.contains(SceneKey.Bouncer) ->
-                                    SceneKey.Gone to "device unlocked in Bouncer scene"
-
-                                // When the device becomes unlocked in Lockscreen, go to Gone if
-                                // bypass is enabled.
-                                renderedScenes.contains(SceneKey.Lockscreen) ->
-                                    if (deviceEntryInteractor.isBypassEnabled.value) {
-                                        SceneKey.Gone to
-                                            "device unlocked in Lockscreen scene with bypass"
-                                    } else {
-                                        null
-                                    }
-
-                                // We got unlocked while on a scene that's not Lockscreen or
-                                // Bouncer, no need to change scenes.
-                                else -> null
-                            }
-
-                        // When the device becomes locked, to Lockscreen.
-                        !isUnlocked ->
-                            when {
-                                // Already on lockscreen or bouncer, no need to change scenes.
-                                renderedScenes.contains(SceneKey.Lockscreen) ||
-                                    renderedScenes.contains(SceneKey.Bouncer) -> null
-
-                                // We got locked while on a scene that's not Lockscreen or Bouncer,
-                                // go to Lockscreen.
-                                else ->
-                                    SceneKey.Lockscreen to
-                                        "device locked in non-Lockscreen and non-Bouncer scene"
-                            }
+                        isOnBouncer ->
+                            // When the device becomes unlocked in Bouncer, go to Gone.
+                            SceneKey.Gone to "device was unlocked in Bouncer scene"
+                        isOnLockscreen ->
+                            // The lockscreen should be dismissed automatically in 2 scenarios:
+                            // 1. When face auth bypass is enabled and authentication happens while
+                            //    the user is on the lockscreen.
+                            // 2. Whenever the user authenticates using an active authentication
+                            //    mechanism like fingerprint auth. Since canSwipeToEnter is true
+                            //    when the user is passively authenticated, the false value here
+                            //    when the unlock state changes indicates this is an active
+                            //    authentication attempt.
+                            if (isBypassEnabled || !canSwipeToEnter)
+                                SceneKey.Gone to
+                                    "device has been unlocked on lockscreen with either " +
+                                        "bypass enabled or using an active authentication mechanism"
+                            else null
+                        // Not on lockscreen or bouncer, so remain in the current scene.
                         else -> null
                     }
                 }
@@ -186,24 +197,15 @@
                         loggingReason = "device is starting to sleep",
                     )
                 } else {
-                    val authMethod = authenticationInteractor.getAuthenticationMethod()
+                    val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
                     val isUnlocked = deviceEntryInteractor.isUnlocked.value
-                    when {
-                        authMethod == AuthenticationMethodModel.None -> {
-                            switchToScene(
-                                targetSceneKey = SceneKey.Gone,
-                                loggingReason =
-                                    "device is starting to wake up while auth method is" + " none",
-                            )
-                        }
-                        authMethod.isSecure && isUnlocked -> {
-                            switchToScene(
-                                targetSceneKey = SceneKey.Gone,
-                                loggingReason =
-                                    "device is starting to wake up while unlocked with a" +
-                                        " secure auth method",
-                            )
-                        }
+                    if (isUnlocked && !canSwipeToEnter) {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Gone,
+                            loggingReason =
+                                "device is waking up while unlocked without the ability" +
+                                    " to swipe up on lockscreen to enter.",
+                        )
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 285cb5a..e9c930a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1483,16 +1483,16 @@
     }
 
     private void updateMaxDisplayedNotifications(boolean recompute) {
+        if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
+            return;
+        }
+
         if (recompute) {
             setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1));
         } else {
             if (SPEW_LOGCAT) Log.d(TAG, "Skipping computeMaxKeyguardNotifications() by request");
         }
 
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
-            return;
-        }
-
         if (isKeyguardShowing() && !mKeyguardBypassController.getBypassEnabled()) {
             mNotificationStackScrollLayoutController.setMaxDisplayedNotifications(
                     mMaxAllowedKeyguardNotifications);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index e2e4556..8bab669 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -65,6 +65,10 @@
      */
     @Deprecated("Use ShadeInteractor instead") val legacyShadeTracking: StateFlow<Boolean>
 
+    /** Specifically tracks the user expanding the shade on the lockscreen only */
+    @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead")
+    val legacyLockscreenShadeTracking: MutableStateFlow<Boolean>
+
     /**
      * QuickSettingsController.mTracking as a flow. "Tracking" means that the user is moving quick
      * settings up or down with a pointer. Going forward, this concept will be replaced by checks
@@ -106,6 +110,9 @@
     /** Sets whether the user is moving the shade with a pointer */
     fun setLegacyShadeTracking(tracking: Boolean)
 
+    /** Sets whether the user is moving the shade with a pointer, on lockscreen only */
+    fun setLegacyLockscreenShadeTracking(tracking: Boolean)
+
     /** Amount shade has expanded with regard to the UDFPS location */
     val udfpsTransitionToFullShadeProgress: StateFlow<Float>
 
@@ -177,6 +184,8 @@
     @Deprecated("Use ShadeInteractor instead")
     override val legacyShadeTracking: StateFlow<Boolean> = _legacyShadeTracking.asStateFlow()
 
+    override val legacyLockscreenShadeTracking = MutableStateFlow(false)
+
     private val _legacyQsTracking = MutableStateFlow(false)
     @Deprecated("Use ShadeInteractor instead")
     override val legacyQsTracking: StateFlow<Boolean> = _legacyQsTracking.asStateFlow()
@@ -212,6 +221,11 @@
         _legacyShadeTracking.value = tracking
     }
 
+    @Deprecated("Should only be called by NPVC and tests")
+    override fun setLegacyLockscreenShadeTracking(tracking: Boolean) {
+        legacyLockscreenShadeTracking.value = tracking
+    }
+
     override fun setQsExpansion(qsExpansion: Float) {
         _qsExpansion.value = qsExpansion
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index b2ffeb3..d687ef6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -72,7 +72,7 @@
     userSetupRepository: UserSetupRepository,
     userSwitcherInteractor: UserSwitcherInteractor,
     sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
-    repository: ShadeRepository,
+    private val repository: ShadeRepository,
 ) {
     /** Emits true if the shade is currently allowed and false otherwise. */
     val isShadeEnabled: StateFlow<Boolean> =
@@ -185,7 +185,15 @@
         if (sceneContainerFlags.isEnabled()) {
             sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.Shade)
         } else {
-            userInteractingFlow(repository.legacyShadeTracking, repository.legacyShadeExpansion)
+            combine(
+                userInteractingFlow(
+                    repository.legacyShadeTracking,
+                    repository.legacyShadeExpansion
+                ),
+                repository.legacyLockscreenShadeTracking
+            ) { legacyShadeTracking, legacyLockscreenShadeTracking ->
+                legacyShadeTracking || legacyLockscreenShadeTracking
+            }
         }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 9c5a201..20b9ede 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shade.ui.viewmodel
 
-import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -34,8 +33,7 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    deviceEntryInteractor: DeviceEntryInteractor,
-    private val bouncerInteractor: BouncerInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
     val shadeHeaderViewModel: ShadeHeaderViewModel,
 ) {
     /** The key of the scene we should switch to when swiping up. */
@@ -60,9 +58,7 @@
             )
 
     /** Notifies that some content in the shade was clicked. */
-    fun onContentClicked() {
-        bouncerInteractor.showOrUnlockDevice()
-    }
+    fun onContentClicked() = deviceEntryInteractor.attemptDeviceEntry()
 
     private fun upDestinationSceneKey(
         isUnlocked: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index bf722af..2e3f3f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -340,6 +340,7 @@
         )
         nsslController.resetScrollPosition()
         nsslController.resetCheckSnoozeLeavebehind()
+        shadeRepository.setLegacyLockscreenShadeTracking(false)
         setDragDownAmountAnimated(0f)
     }
 
@@ -366,6 +367,7 @@
                 cancel()
             }
         }
+        shadeRepository.setLegacyLockscreenShadeTracking(true)
     }
 
     /** Do we need a falsing check currently? */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 3f080c2..0e83c78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -680,7 +680,7 @@
         }
         NotificationEntry entry = mCommonNotifCollectionLazy.get().getEntry(key);
         if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            return entry != null
+            return entry != null && entry.getRanking().getChannel() != null
                     && entry.getRanking().getChannel().getLockscreenVisibility()
                     == Notification.VISIBILITY_PRIVATE;
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
new file mode 100644
index 0000000..6af2543
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.interruption
+
+import android.app.NotificationManager.IMPORTANCE_HIGH
+import android.os.PowerManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_DREAMING
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_NOT_INTERACTIVE
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_NOT_PROVISIONED
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_KEYGUARD_OCCLUDED
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_KEYGUARD_SHOWING
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_LOCKED_SHADE
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_EXPECTED_TO_HUN
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NOT_IMPORTANT_ENOUGH
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NO_FULL_SCREEN_INTENT
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NO_HUN_OR_KEYGUARD
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_PACKAGE_SUSPENDED
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SHOW_STICKY_HUN
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_BY_DND
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_ONLY_BY_DND
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+
+class FullScreenIntentDecisionProvider(
+    private val deviceProvisionedController: DeviceProvisionedController,
+    private val keyguardStateController: KeyguardStateController,
+    private val powerManager: PowerManager,
+    private val statusBarStateController: StatusBarStateController
+) {
+    interface Decision {
+        val shouldFsi: Boolean
+        val wouldFsiWithoutDnd: Boolean
+        val logReason: String
+    }
+
+    private enum class DecisionImpl(
+        override val shouldFsi: Boolean,
+        override val logReason: String,
+        override val wouldFsiWithoutDnd: Boolean = shouldFsi,
+        val supersedesDnd: Boolean = false
+    ) : Decision {
+        NO_FSI_NO_FULL_SCREEN_INTENT(false, "no full-screen intent", supersedesDnd = true),
+        NO_FSI_SHOW_STICKY_HUN(false, "full-screen intents are disabled", supersedesDnd = true),
+        NO_FSI_NOT_IMPORTANT_ENOUGH(false, "not important enough"),
+        NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(false, "suppressive group alert behavior"),
+        NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false, "suppressive bubble metadata"),
+        NO_FSI_PACKAGE_SUSPENDED(false, "package suspended"),
+        FSI_DEVICE_NOT_INTERACTIVE(true, "device is not interactive"),
+        FSI_DEVICE_DREAMING(true, "device is dreaming"),
+        FSI_KEYGUARD_SHOWING(true, "keyguard is showing"),
+        NO_FSI_EXPECTED_TO_HUN(false, "expected to heads-up instead"),
+        FSI_KEYGUARD_OCCLUDED(true, "keyguard is occluded"),
+        FSI_LOCKED_SHADE(true, "locked shade"),
+        FSI_DEVICE_NOT_PROVISIONED(true, "device not provisioned"),
+        NO_FSI_NO_HUN_OR_KEYGUARD(false, "no HUN or keyguard"),
+        NO_FSI_SUPPRESSED_BY_DND(false, "suppressed by DND", wouldFsiWithoutDnd = false),
+        NO_FSI_SUPPRESSED_ONLY_BY_DND(false, "suppressed only by DND", wouldFsiWithoutDnd = true)
+    }
+
+    fun makeFullScreenIntentDecision(entry: NotificationEntry, couldHeadsUp: Boolean): Decision {
+        val reasonWithoutDnd = makeDecisionWithoutDnd(entry, couldHeadsUp)
+
+        val suppressedWithoutDnd = !reasonWithoutDnd.shouldFsi
+        val suppressedByDnd = entry.shouldSuppressFullScreenIntent()
+
+        val reasonWithDnd =
+            when {
+                reasonWithoutDnd.supersedesDnd -> reasonWithoutDnd
+                suppressedByDnd && !suppressedWithoutDnd -> NO_FSI_SUPPRESSED_ONLY_BY_DND
+                suppressedByDnd -> NO_FSI_SUPPRESSED_BY_DND
+                else -> reasonWithoutDnd
+            }
+
+        return reasonWithDnd
+    }
+
+    private fun makeDecisionWithoutDnd(
+        entry: NotificationEntry,
+        couldHeadsUp: Boolean
+    ): DecisionImpl {
+        val sbn = entry.sbn
+        val notification = sbn.notification!!
+
+        if (notification.fullScreenIntent == null) {
+            return if (entry.isStickyAndNotDemoted) {
+                NO_FSI_SHOW_STICKY_HUN
+            } else {
+                NO_FSI_NO_FULL_SCREEN_INTENT
+            }
+        }
+
+        if (entry.importance < IMPORTANCE_HIGH) {
+            return NO_FSI_NOT_IMPORTANT_ENOUGH
+        }
+
+        if (sbn.isGroup && notification.suppressAlertingDueToGrouping()) {
+            return NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+        }
+
+        val bubbleMetadata = notification.bubbleMetadata
+        if (bubbleMetadata != null && bubbleMetadata.isNotificationSuppressed) {
+            return NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
+        }
+
+        if (entry.ranking.isSuspended) {
+            return NO_FSI_PACKAGE_SUSPENDED
+        }
+
+        if (!powerManager.isInteractive) {
+            return FSI_DEVICE_NOT_INTERACTIVE
+        }
+
+        if (statusBarStateController.isDreaming) {
+            return FSI_DEVICE_DREAMING
+        }
+
+        if (statusBarStateController.state == KEYGUARD) {
+            return FSI_KEYGUARD_SHOWING
+        }
+
+        if (couldHeadsUp) {
+            return NO_FSI_EXPECTED_TO_HUN
+        }
+
+        if (keyguardStateController.isShowing) {
+            return if (keyguardStateController.isOccluded) {
+                FSI_KEYGUARD_OCCLUDED
+            } else {
+                FSI_LOCKED_SHADE
+            }
+        }
+
+        if (!deviceProvisionedController.isDeviceProvisioned) {
+            return FSI_DEVICE_NOT_PROVISIONED
+        }
+
+        return NO_FSI_NO_HUN_OR_KEYGUARD
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 2730683..9640682 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -18,7 +18,6 @@
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.Handler
 import android.os.PowerManager
-import android.util.Log
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -30,7 +29,9 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
 import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
@@ -40,9 +41,11 @@
 constructor(
     private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
     private val batteryController: BatteryController,
+    deviceProvisionedController: DeviceProvisionedController,
     private val globalSettings: GlobalSettings,
     private val headsUpManager: HeadsUpManager,
     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
+    keyguardStateController: KeyguardStateController,
     private val logger: NotificationInterruptLogger,
     @Main private val mainHandler: Handler,
     private val powerManager: PowerManager,
@@ -50,6 +53,36 @@
     private val systemClock: SystemClock,
     private val userTracker: UserTracker,
 ) : VisualInterruptionDecisionProvider {
+    private class DecisionImpl(
+        override val shouldInterrupt: Boolean,
+        override val logReason: String
+    ) : Decision
+
+    private class FullScreenIntentDecisionImpl(
+        private val fsiDecision: FullScreenIntentDecisionProvider.Decision
+    ) : FullScreenIntentDecision {
+        override val shouldInterrupt
+            get() = fsiDecision.shouldFsi
+
+        override val wouldInterruptWithoutDnd
+            get() = fsiDecision.wouldFsiWithoutDnd
+
+        override val logReason
+            get() = fsiDecision.logReason
+    }
+
+    private val fullScreenIntentDecisionProvider =
+        FullScreenIntentDecisionProvider(
+            deviceProvisionedController,
+            keyguardStateController,
+            powerManager,
+            statusBarStateController
+        )
+
+    private val legacySuppressors = mutableSetOf<NotificationInterruptSuppressor>()
+    private val conditions = mutableListOf<VisualInterruptionCondition>()
+    private val filters = mutableListOf<VisualInterruptionFilter>()
+
     private var started = false
 
     override fun start() {
@@ -76,24 +109,6 @@
         started = true
     }
 
-    private class DecisionImpl(
-        override val shouldInterrupt: Boolean,
-        override val logReason: String
-    ) : Decision
-
-    private class FullScreenIntentDecisionImpl(
-        override val shouldInterrupt: Boolean,
-        override val wouldInterruptWithoutDnd: Boolean,
-        override val logReason: String,
-        val originalEntry: NotificationEntry,
-    ) : FullScreenIntentDecision {
-        var hasBeenLogged = false
-    }
-
-    private val legacySuppressors = mutableSetOf<NotificationInterruptSuppressor>()
-    private val conditions = mutableListOf<VisualInterruptionCondition>()
-    private val filters = mutableListOf<VisualInterruptionFilter>()
-
     override fun addLegacySuppressor(suppressor: NotificationInterruptSuppressor) {
         legacySuppressors.add(suppressor)
     }
@@ -132,32 +147,21 @@
         return makeHeadsUpDecision(entry).also { logHeadsUpDecision(entry, it) }
     }
 
+    override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision {
+        check(started)
+        return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) }
+    }
+
     override fun makeUnloggedFullScreenIntentDecision(
         entry: NotificationEntry
     ): FullScreenIntentDecision {
         check(started)
-        return makeFullScreenDecision(entry)
+        return makeFullScreenIntentDecision(entry)
     }
 
     override fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) {
         check(started)
-        val decisionImpl =
-            decision as? FullScreenIntentDecisionImpl
-                ?: run {
-                    Log.wtf(TAG, "Wrong subclass of FullScreenIntentDecision: $decision")
-                    return
-                }
-        if (decision.hasBeenLogged) {
-            Log.wtf(TAG, "Already logged decision: $decision")
-            return
-        }
-        logFullScreenIntentDecision(decisionImpl)
-        decision.hasBeenLogged = true
-    }
-
-    override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision {
-        check(started)
-        return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) }
+        // Not yet implemented.
     }
 
     private fun makeHeadsUpDecision(entry: NotificationEntry): DecisionImpl {
@@ -234,16 +238,6 @@
         return DecisionImpl(shouldInterrupt = true, logReason = "not suppressed")
     }
 
-    private fun makeFullScreenDecision(entry: NotificationEntry): FullScreenIntentDecisionImpl {
-        // Not yet implemented.
-        return FullScreenIntentDecisionImpl(
-            shouldInterrupt = true,
-            wouldInterruptWithoutDnd = true,
-            logReason = "FSI logic not yet implemented in VisualInterruptionDecisionProviderImpl",
-            originalEntry = entry
-        )
-    }
-
     private fun logHeadsUpDecision(entry: NotificationEntry, decision: DecisionImpl) {
         // Not yet implemented.
     }
@@ -252,8 +246,11 @@
         // Not yet implemented.
     }
 
-    private fun logFullScreenIntentDecision(decision: FullScreenIntentDecisionImpl) {
-        // Not yet implemented.
+    private fun makeFullScreenIntentDecision(entry: NotificationEntry): FullScreenIntentDecision {
+        val wouldHeadsUp = makeUnloggedHeadsUpDecision(entry).shouldInterrupt
+        val fsiDecision =
+            fullScreenIntentDecisionProvider.makeFullScreenIntentDecision(entry, wouldHeadsUp)
+        return FullScreenIntentDecisionImpl(fsiDecision)
     }
 
     private fun checkSuppressors(entry: NotificationEntry) =
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 a0ffba3..14ec08f35 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
@@ -253,6 +253,7 @@
     private NotificationLogger.OnChildLocationsChangedListener mListener;
     private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
     private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
+    private Runnable mOnHeightChangedRunnable;
     private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
     private boolean mNeedsAnimation;
     private boolean mTopPaddingNeedsAnimation;
@@ -1121,6 +1122,10 @@
         if (mOnHeightChangedListener != null) {
             mOnHeightChangedListener.onHeightChanged(view, needsAnimation);
         }
+
+        if (mOnHeightChangedRunnable != null) {
+            mOnHeightChangedRunnable.run();
+        }
     }
 
     public boolean isPulseExpanding() {
@@ -4252,6 +4257,10 @@
         this.mOnHeightChangedListener = onHeightChangedListener;
     }
 
+    void setOnHeightChangedRunnable(Runnable r) {
+        this.mOnHeightChangedRunnable = r;
+    }
+
     void onChildAnimationFinished() {
         setAnimationRunning(false);
         requestChildrenUpdate();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 99b3a00..2cf0c26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -20,6 +20,7 @@
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
 import static com.android.app.animation.Interpolators.STANDARD;
+
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -34,7 +35,6 @@
 
 import android.animation.ObjectAnimator;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.graphics.Point;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -65,9 +65,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
@@ -94,7 +92,6 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -112,7 +109,6 @@
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.dagger.SilentHeader;
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -122,11 +118,9 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -960,6 +954,13 @@
         mView.setOnHeightChangedListener(listener);
     }
 
+    /**
+     * Invoked in addition to {@see #setOnHeightChangedListener}
+     */
+    public void setOnHeightChangedRunnable(Runnable r) {
+        mView.setOnHeightChangedRunnable(r);
+    }
+
     public void setOverscrollTopChangedListener(
             OnOverscrollTopChangedListener listener) {
         mView.setOverscrollTopChangedListener(listener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 57cea5d..eb1c17a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -18,13 +18,15 @@
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
 import android.content.Context
-import com.android.systemui.res.R
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
@@ -43,6 +45,10 @@
     private val _topPosition = MutableStateFlow(0f)
     val topPosition = _topPosition.asStateFlow()
 
+    private val _notificationStackChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+    /** An internal modification was made to notifications */
+    val notificationStackChanged = _notificationStackChanged.asSharedFlow()
+
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
         configurationRepository.onAnyConfigurationChange
             .onStart { emit(Unit) }
@@ -72,6 +78,11 @@
         _topPosition.value = top
     }
 
+    /** An internal modification was made to notifications */
+    fun notificationStackChanged() {
+        _notificationStackChanged.tryEmit(Unit)
+    }
+
     data class ConfigurationBasedDimensions(
         val useSplitShade: Boolean,
         val useLargeScreenHeader: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index a1a0cca..0ff1bec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import kotlinx.coroutines.DisposableHandle
@@ -33,6 +34,7 @@
         view: SharedNotificationContainer,
         viewModel: SharedNotificationContainerViewModel,
         controller: NotificationStackScrollLayoutController,
+        notificationStackSizeCalculator: NotificationStackSizeCalculator,
     ): DisposableHandle {
         val disposableHandle =
             view.repeatWhenAttached {
@@ -54,9 +56,16 @@
                     }
 
                     launch {
-                        viewModel.maxNotifications.collect {
-                            controller.setMaxDisplayedNotifications(it)
-                        }
+                        viewModel
+                            .getMaxNotifications { space ->
+                                notificationStackSizeCalculator.computeMaxKeyguardNotifications(
+                                    controller.getView(),
+                                    space,
+                                    0f, // Vertical space for shelf is already accounted for
+                                    controller.getShelfHeight().toFloat(),
+                                )
+                            }
+                            .collect { controller.setMaxDisplayedNotifications(it) }
                     }
 
                     launch {
@@ -70,9 +79,12 @@
                 }
             }
 
+        controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+
         return object : DisposableHandle {
             override fun dispose() {
                 disposableHandle.dispose()
+                controller.setOnHeightChangedRunnable(null)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index b86b5dc..d6b6f75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -22,28 +22,26 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 
 /** View-model for the shared notification container, used by both the shade and keyguard spaces */
 class SharedNotificationContainerViewModel
 @Inject
 constructor(
-    interactor: SharedNotificationContainerInteractor,
+    private val interactor: SharedNotificationContainerInteractor,
     keyguardInteractor: KeyguardInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    controller: NotificationStackScrollLayoutController,
-    shadeInteractor: ShadeInteractor,
+    private val shadeInteractor: ShadeInteractor,
 ) {
     private val statesForConstrainedNotifications =
         setOf(
@@ -151,24 +149,46 @@
      * When on keyguard, there is limited space to display notifications so calculate how many could
      * be shown. Otherwise, there is no limit since the vertical space will be scrollable.
      *
-     * TODO: b/296606746 - Need to rerun logic when notifs change
+     * When expanding or when the user is interacting with the shade, keep the count stable; do not
+     * emit a value.
      */
-    val maxNotifications: Flow<Int> =
-        combine(isOnLockscreen, shadeInteractor.shadeExpansion, position) {
-            onLockscreen,
-            shadeExpansion,
-            positionInfo ->
-            if (onLockscreen && shadeExpansion < 1f) {
-                notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                    controller.getView(),
-                    positionInfo.bottom - positionInfo.top,
-                    0f, // Vertical space for shelf is already accounted for
-                    controller.getShelfHeight().toFloat(),
-                )
-            } else {
-                -1 // No limit
+    fun getMaxNotifications(calculateSpace: (Float) -> Int): Flow<Int> {
+        // When to limit notifications: on lockscreen with an unexpanded shade. Also, recalculate
+        // when the notification stack has changed internally
+        val limitedNotifications =
+            combineTransform(
+                isOnLockscreen,
+                position,
+                shadeInteractor.shadeExpansion,
+                interactor.notificationStackChanged.onStart { emit(Unit) },
+            ) { isOnLockscreen, position, shadeExpansion, _ ->
+                if (isOnLockscreen && shadeExpansion == 0f) {
+                    emit(calculateSpace(position.bottom - position.top))
+                }
             }
-        }
+
+        // When to show unlimited notifications: When the shade is fully expanded and the user is
+        // not actively dragging the shade
+        val unlimitedNotifications =
+            combineTransform(
+                shadeInteractor.shadeExpansion,
+                shadeInteractor.isUserInteracting,
+            ) { shadeExpansion, isUserInteracting ->
+                if (shadeExpansion == 1f && !isUserInteracting) {
+                    emit(-1)
+                }
+            }
+
+        return merge(
+                limitedNotifications,
+                unlimitedNotifications,
+            )
+            .distinctUntilChanged()
+    }
+
+    fun notificationStackChanged() {
+        interactor.notificationStackChanged()
+    }
 
     data class ConfigurationBasedDimensions(
         val marginStart: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
index db4ab7e..5b91615 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
@@ -25,6 +25,16 @@
  * The fraction after which we start fading in when going from a gone widget to a visible one
  */
 private const val GONE_FADE_FRACTION = 0.8f
+/**
+ * The fraction after which we start fading in going from a gone widget to a visible one in guts
+ * animation.
+ */
+private const val GONE_FADE_GUTS_FRACTION = 0.286f
+/**
+ * The fraction before which we fade out when going from a visible widget to a gone one in guts
+ * animation.
+ */
+private const val VISIBLE_FADE_GUTS_FRACTION = 0.355f
 
 /**
  * The amont we're scaling appearing views
@@ -45,6 +55,7 @@
     private var animationStartState: TransitionViewState? = null
     private var state = TransitionViewState()
     private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
+    private var isGutsAnimation = false
     private var currentHeight: Int = 0
     private var currentWidth: Int = 0
     var sizeChangedListener: ((Int, Int) -> Unit)? = null
@@ -152,15 +163,6 @@
                 // this looks quite ugly
                 val nowGone: Boolean
                 if (widgetStart.gone) {
-
-                    // Only fade it in at the very end
-                    alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress)
-                    nowGone = progress < GONE_FADE_FRACTION
-
-                    // Scale it just a little, not all the way
-                    val endScale = widgetEnd.scale
-                    newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
-
                     // don't clip
                     widthProgress = 1.0f
 
@@ -168,25 +170,52 @@
                     resultMeasureWidth = widgetEnd.measureWidth
                     resultMeasureHeight = widgetEnd.measureHeight
 
-                    // Let's make sure we're centering the view in the gone view instead of having
-                    // the left at 0
-                    resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f,
-                            widgetEnd.x,
-                            progress)
-                    resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f,
-                            widgetEnd.y,
-                            progress)
+                    if (isGutsAnimation) {
+                        // if animation is open/close guts, fade in starts early.
+                        alphaProgress = MathUtils.map(
+                            GONE_FADE_GUTS_FRACTION,
+                            1.0f,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress < GONE_FADE_GUTS_FRACTION
+
+                        // Do not change scale of widget.
+                        newScale = 1.0f
+
+                        // We do not want any horizontal or vertical movement.
+                        resultX = widgetStart.x
+                        resultY = widgetStart.y
+                    } else {
+                        // Only fade it in at the very end
+                        alphaProgress = MathUtils.map(
+                            GONE_FADE_FRACTION,
+                            1.0f,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress < GONE_FADE_FRACTION
+
+                        // Scale it just a little, not all the way
+                        val endScale = widgetEnd.scale
+                        newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
+
+                        // Let's make sure we're centering the view in the gone view instead of
+                        // having the left at 0
+                        resultX = MathUtils.lerp(
+                                widgetStart.x - resultMeasureWidth / 2.0f,
+                                widgetEnd.x,
+                                progress
+                        )
+                        resultY = MathUtils.lerp(
+                                widgetStart.y - resultMeasureHeight / 2.0f,
+                                widgetEnd.y,
+                                progress
+                        )
+                    }
                 } else {
-
-                    // Fadeout in the very beginning
-                    alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f,
-                            progress)
-                    nowGone = progress > 1.0f - GONE_FADE_FRACTION
-
-                    // Scale it just a little, not all the way
-                    val startScale = widgetStart.scale
-                    newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress)
-
                     // Don't clip
                     widthProgress = 0.0f
 
@@ -194,14 +223,54 @@
                     resultMeasureWidth = widgetStart.measureWidth
                     resultMeasureHeight = widgetStart.measureHeight
 
-                    // Let's make sure we're centering the view in the gone view instead of having
-                    // the left at 0
-                    resultX = MathUtils.lerp(widgetStart.x,
-                            widgetEnd.x - resultMeasureWidth / 2.0f,
-                            progress)
-                    resultY = MathUtils.lerp(widgetStart.y,
-                            widgetEnd.y - resultMeasureHeight / 2.0f,
-                            progress)
+                    // Fadeout in the very beginning
+                    if (isGutsAnimation) {
+                        alphaProgress = MathUtils.map(
+                            0.0f,
+                            VISIBLE_FADE_GUTS_FRACTION,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress > VISIBLE_FADE_GUTS_FRACTION
+
+                        // Do not change scale of widget during open/close guts animation.
+                        newScale = 1.0f
+
+                        // We do not want any horizontal or vertical movement.
+                        resultX = widgetEnd.x
+                        resultY = widgetEnd.y
+                    } else {
+                        alphaProgress = MathUtils.map(
+                            0.0f,
+                            1.0f - GONE_FADE_FRACTION,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress > 1.0f - GONE_FADE_FRACTION
+
+                        // Scale it just a little, not all the way
+                        val startScale = widgetStart.scale
+                        newScale = MathUtils.lerp(
+                                startScale,
+                                startScale * GONE_SCALE_AMOUNT,
+                                progress
+                        )
+
+                        // Let's make sure we're centering the view in the gone view instead of
+                        // having the left at 0
+                        resultX = MathUtils.lerp(
+                                widgetStart.x,
+                                widgetEnd.x - resultMeasureWidth / 2.0f,
+                                progress
+                        )
+                        resultY = MathUtils.lerp(
+                                widgetStart.y,
+                                widgetEnd.y - resultMeasureHeight / 2.0f,
+                                progress
+                        )
+                    }
                 }
                 resultWidgetState.gone = nowGone
             } else {
@@ -279,8 +348,10 @@
         applyImmediately: Boolean,
         animate: Boolean,
         duration: Long = 0,
-        delay: Long = 0
+        delay: Long = 0,
+        isGuts: Boolean,
     ) {
+        isGutsAnimation = isGuts
         val animated = animate && currentState.width != 0
         this.state = state.copy()
         if (applyImmediately || transitionLayout == null) {
@@ -291,6 +362,8 @@
             animationStartState = currentState.copy()
             animator.duration = duration
             animator.startDelay = delay
+            animator.interpolator =
+                if (isGutsAnimation) Interpolators.LINEAR else Interpolators.FAST_OUT_SLOW_IN
             animator.start()
         } else if (!animator.isRunning) {
             applyStateToLayout(this.state)
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index 1d4f2cb..d2f45ae 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -42,8 +42,8 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.AuthRippleController;
-import com.android.systemui.bouncer.domain.interactor.BouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
 import com.android.systemui.doze.util.BurnInHelperKt;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -78,7 +78,7 @@
     protected MockitoSession mStaticMockSession;
 
     protected final SceneTestUtils mSceneTestUtils = new SceneTestUtils(this);
-    protected @Mock BouncerInteractor mBouncerInteractor;
+    protected @Mock DeviceEntryInteractor mDeviceEntryInteractor;
     protected @Mock LockIconView mLockIconView;
     protected @Mock AnimatedStateListDrawable mIconDrawable;
     protected @Mock Context mContext;
@@ -176,7 +176,7 @@
                 mFeatureFlags,
                 mPrimaryBouncerInteractor,
                 mContext,
-                () -> mBouncerInteractor,
+                () -> mDeviceEntryInteractor,
                 mSceneTestUtils.getSceneContainerFlags()
         );
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
index adcec10..93a5393 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -381,7 +381,7 @@
 
         // THEN show primary bouncer via keyguard view controller, not scene container
         verify(mKeyguardViewController).showPrimaryBouncer(anyBoolean());
-        verify(mBouncerInteractor, never()).showOrUnlockDevice(any());
+        verify(mDeviceEntryInteractor, never()).attemptDeviceEntry();
     }
 
     @Test
@@ -395,7 +395,7 @@
 
         // THEN show primary bouncer
         verify(mKeyguardViewController, never()).showPrimaryBouncer(anyBoolean());
-        verify(mBouncerInteractor).showOrUnlockDevice(any());
+        verify(mDeviceEntryInteractor).attemptDeviceEntry();
     }
 
     @Test
@@ -408,6 +408,7 @@
         mUnderTest.onLongPress();
 
         // THEN don't show primary bouncer
-        verify(mBouncerInteractor, never()).showOrUnlockDevice(any());
+        verify(mDeviceEntryInteractor, never()).attemptDeviceEntry();
+        verify(mKeyguardViewController, never()).showPrimaryBouncer(anyBoolean());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index 6ac84bc..ae2ec2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -26,7 +26,7 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.scene.SceneTestUtils
@@ -100,12 +100,12 @@
         }
 
     @Test
-    fun isAutoConfirmEnabled() =
+    fun isAutoConfirmFeatureEnabled() =
         testScope.runTest {
             whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[0].id)).thenReturn(true)
             whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[1].id)).thenReturn(false)
 
-            val values by collectValues(underTest.isAutoConfirmEnabled)
+            val values by collectValues(underTest.isAutoConfirmFeatureEnabled)
             assertThat(values.first()).isFalse()
             assertThat(values.last()).isTrue()
 
@@ -127,6 +127,22 @@
             assertThat(values.last()).isTrue()
         }
 
+    @Test
+    fun reportAuthenticationAttempt_emitsAuthenticationChallengeResult() =
+        testScope.runTest {
+            val authenticationChallengeResults by
+                collectValues(underTest.authenticationChallengeResult)
+
+            runCurrent()
+            underTest.reportAuthenticationAttempt(true)
+            runCurrent()
+            underTest.reportAuthenticationAttempt(false)
+            runCurrent()
+            underTest.reportAuthenticationAttempt(true)
+
+            assertThat(authenticationChallengeResults).isEqualTo(listOf(true, false, true))
+        }
+
     private fun setSecurityModeAndDispatchBroadcast(
         securityMode: KeyguardSecurityModel.SecurityMode,
     ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 2f5f460..7439db2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -20,9 +20,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.coroutines.collectLastValue
@@ -51,33 +50,16 @@
         testScope.runTest {
             val authMethod by collectLastValue(underTest.authenticationMethod)
             runCurrent()
-            assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Pin)
-            assertThat(underTest.getAuthenticationMethod())
-                .isEqualTo(DomainLayerAuthenticationMethodModel.Pin)
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin)
+            assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Pin)
 
             utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Password
+                AuthenticationMethodModel.Password
             )
 
-            assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Password)
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password)
             assertThat(underTest.getAuthenticationMethod())
-                .isEqualTo(DomainLayerAuthenticationMethodModel.Password)
-        }
-
-    @Test
-    fun authenticationMethod_noneTreatedAsSwipe_whenLockscreenEnabled() =
-        testScope.runTest {
-            val authMethod by collectLastValue(underTest.authenticationMethod)
-            runCurrent()
-
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.None
-            )
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
-
-            assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Swipe)
-            assertThat(underTest.getAuthenticationMethod())
-                .isEqualTo(DomainLayerAuthenticationMethodModel.Swipe)
+                .isEqualTo(AuthenticationMethodModel.Password)
         }
 
     @Test
@@ -86,23 +68,18 @@
             val authMethod by collectLastValue(underTest.authenticationMethod)
             runCurrent()
 
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.None
-            )
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(false)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
 
-            assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.None)
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None)
             assertThat(underTest.getAuthenticationMethod())
-                .isEqualTo(DomainLayerAuthenticationMethodModel.None)
+                .isEqualTo(AuthenticationMethodModel.None)
         }
 
     @Test
     fun authenticate_withCorrectPin_returnsTrue() =
         testScope.runTest {
             val isThrottled by collectLastValue(underTest.isThrottled)
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(isThrottled).isFalse()
@@ -111,9 +88,7 @@
     @Test
     fun authenticate_withIncorrectPin_returnsFalse() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4)))
                 .isEqualTo(AuthenticationResult.FAILED)
         }
@@ -121,9 +96,7 @@
     @Test(expected = IllegalArgumentException::class)
     fun authenticate_withEmptyPin_throwsException() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             underTest.authenticate(listOf())
         }
 
@@ -132,7 +105,7 @@
         testScope.runTest {
             val pin = List(16) { 9 }
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
                 overrideCredential(pin)
             }
 
@@ -148,9 +121,7 @@
             // If the policy changes, there is work to do in SysUI.
             assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17)
 
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(underTest.authenticate(List(17) { 9 }))
                 .isEqualTo(AuthenticationResult.FAILED)
         }
@@ -160,7 +131,7 @@
         testScope.runTest {
             val isThrottled by collectLastValue(underTest.isThrottled)
             utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Password
+                AuthenticationMethodModel.Password
             )
 
             assertThat(underTest.authenticate("password".toList()))
@@ -172,7 +143,7 @@
     fun authenticate_withIncorrectPassword_returnsFalse() =
         testScope.runTest {
             utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Password
+                AuthenticationMethodModel.Password
             )
 
             assertThat(underTest.authenticate("alohomora".toList()))
@@ -183,7 +154,7 @@
     fun authenticate_withCorrectPattern_returnsTrue() =
         testScope.runTest {
             utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pattern
+                AuthenticationMethodModel.Pattern
             )
 
             assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
@@ -194,7 +165,7 @@
     fun authenticate_withIncorrectPattern_returnsFalse() =
         testScope.runTest {
             utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pattern
+                AuthenticationMethodModel.Pattern
             )
 
             assertThat(
@@ -211,13 +182,16 @@
         }
 
     @Test
-    fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNullAndHasNoEffect() =
+    fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNull() =
         testScope.runTest {
+            val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
             val isThrottled by collectLastValue(underTest.isThrottled)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
-                setAutoConfirmEnabled(true)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(true)
             }
+            assertThat(isAutoConfirmEnabled).isTrue()
+
             assertThat(
                     underTest.authenticate(
                         FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply {
@@ -231,12 +205,15 @@
         }
 
     @Test
-    fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalseAndDoesNotUnlockDevice() =
+    fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalse() =
         testScope.runTest {
+            val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
-                setAutoConfirmEnabled(true)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(true)
             }
+            assertThat(isAutoConfirmEnabled).isTrue()
+
             assertThat(
                     underTest.authenticate(
                         FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 },
@@ -244,17 +221,18 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.FAILED)
-            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
-            assertThat(isUnlocked).isFalse()
         }
 
     @Test
-    fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalseAndDoesNotUnlockDevice() =
+    fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalse() =
         testScope.runTest {
+            val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
-                setAutoConfirmEnabled(true)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(true)
             }
+            assertThat(isAutoConfirmEnabled).isTrue()
+
             assertThat(
                     underTest.authenticate(
                         FakeAuthenticationRepository.DEFAULT_PIN + listOf(7),
@@ -262,17 +240,18 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.FAILED)
-            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
-            assertThat(isUnlocked).isFalse()
         }
 
     @Test
-    fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrueAndUnlocksDevice() =
+    fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrue() =
         testScope.runTest {
+            val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
-                setAutoConfirmEnabled(true)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(true)
             }
+            assertThat(isAutoConfirmEnabled).isTrue()
+
             assertThat(
                     underTest.authenticate(
                         FakeAuthenticationRepository.DEFAULT_PIN,
@@ -280,16 +259,38 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
-            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
-            assertThat(isUnlocked).isTrue()
         }
 
     @Test
-    fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNullAndHasNoEffects() =
+    fun tryAutoConfirm_withAutoConfirmCorrectPinButDuringThrottling_returnsNull() =
+        testScope.runTest {
+            val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
+            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
+            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
+            utils.authenticationRepository.apply {
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(true)
+                setThrottleDuration(42)
+            }
+
+            val authResult =
+                underTest.authenticate(
+                    FakeAuthenticationRepository.DEFAULT_PIN,
+                    tryAutoConfirm = true
+                )
+
+            assertThat(authResult).isEqualTo(AuthenticationResult.SKIPPED)
+            assertThat(isAutoConfirmEnabled).isFalse()
+            assertThat(isUnlocked).isFalse()
+            assertThat(hintedPinLength).isNull()
+        }
+
+    @Test
+    fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNull() =
         testScope.runTest {
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
-                setAutoConfirmEnabled(false)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(false)
             }
             assertThat(
                     underTest.authenticate(
@@ -298,53 +299,38 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
-            assertThat(isUnlocked).isFalse()
         }
 
     @Test
-    fun tryAutoConfirm_withoutCorrectPassword_returnsNullAndHasNoEffects() =
+    fun tryAutoConfirm_withoutCorrectPassword_returnsNull() =
         testScope.runTest {
             utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Password
+                AuthenticationMethodModel.Password
             )
 
             assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
-            assertThat(isUnlocked).isFalse()
         }
 
     @Test
     fun throttling() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
             val throttling by collectLastValue(underTest.throttling)
             val isThrottled by collectLastValue(underTest.isThrottled)
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
-            assertThat(isUnlocked).isTrue()
-            assertThat(isThrottled).isFalse()
-            assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
-
-            utils.deviceEntryRepository.setUnlocked(false)
-            assertThat(isUnlocked).isFalse()
             assertThat(isThrottled).isFalse()
             assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
 
             // Make many wrong attempts, but just shy of what's needed to get throttled:
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING - 1) {
                 underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
-                assertThat(isUnlocked).isFalse()
                 assertThat(isThrottled).isFalse()
                 assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
             }
 
             // Make one more wrong attempt, leading to throttling:
             underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
-            assertThat(isUnlocked).isFalse()
             assertThat(isThrottled).isTrue()
             assertThat(throttling)
                 .isEqualTo(
@@ -358,7 +344,6 @@
             // Correct PIN, but throttled, so doesn't attempt it:
             assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(isUnlocked).isFalse()
             assertThat(isThrottled).isTrue()
             assertThat(throttling)
                 .isEqualTo(
@@ -391,7 +376,6 @@
 
             // Move the clock forward one more second, to completely finish the throttling period:
             advanceTimeBy(1000)
-            assertThat(isUnlocked).isFalse()
             assertThat(isThrottled).isFalse()
             assertThat(throttling)
                 .isEqualTo(
@@ -405,7 +389,6 @@
             // Correct PIN and no longer throttled so unlocks successfully:
             assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(isUnlocked).isTrue()
             assertThat(isThrottled).isFalse()
             assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
         }
@@ -415,8 +398,8 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
-                setAutoConfirmEnabled(false)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(false)
             }
 
             assertThat(hintedPinLength).isNull()
@@ -427,13 +410,13 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
                 overrideCredential(
                     buildList {
                         repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) }
                     }
                 )
-                setAutoConfirmEnabled(true)
+                setAutoConfirmFeatureEnabled(true)
             }
 
             assertThat(hintedPinLength).isNull()
@@ -444,8 +427,8 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
-                setAutoConfirmEnabled(true)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAutoConfirmFeatureEnabled(true)
                 overrideCredential(
                     buildList {
                         repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) }
@@ -461,13 +444,13 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
+                setAuthenticationMethod(AuthenticationMethodModel.Pin)
                 overrideCredential(
                     buildList {
                         repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) }
                     }
                 )
-                setAutoConfirmEnabled(true)
+                setAutoConfirmFeatureEnabled(true)
             }
 
             assertThat(hintedPinLength).isNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 915661b..6ead0e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -19,16 +19,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
-import com.android.systemui.scene.shared.model.SceneKey
-import com.android.systemui.scene.shared.model.SceneModel
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.ceil
 import kotlin.time.Duration.Companion.milliseconds
@@ -48,17 +46,9 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val sceneInteractor = utils.sceneInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-        )
     private val underTest =
         utils.bouncerInteractor(
-            deviceEntryInteractor = deviceEntryInteractor,
             authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
         )
 
     @Before
@@ -74,16 +64,10 @@
     @Test
     fun pinAuthMethod() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(underTest.message)
 
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             runCurrent()
-            utils.deviceEntryRepository.setUnlocked(false)
-            underTest.showOrUnlockDevice()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
             underTest.clearMessage()
             assertThat(message).isEmpty()
 
@@ -94,7 +78,6 @@
             assertThat(underTest.authenticate(listOf(9, 8, 7)))
                 .isEqualTo(AuthenticationResult.FAILED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
@@ -102,35 +85,25 @@
             // Correct input.
             assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
     @Test
     fun pinAuthMethod_tryAutoConfirm_withAutoConfirmPin() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            val message by collectLastValue(underTest.message)
+            val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
 
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             runCurrent()
-            utils.authenticationRepository.setAutoConfirmEnabled(true)
-            utils.deviceEntryRepository.setUnlocked(false)
-            underTest.showOrUnlockDevice()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-            underTest.clearMessage()
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
+            assertThat(isAutoConfirmEnabled).isTrue()
 
             // Incomplete input.
             assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Wrong 6-digit pin
             assertThat(underTest.authenticate(listOf(1, 2, 3, 5, 5, 6), tryAutoConfirm = true))
                 .isEqualTo(AuthenticationResult.FAILED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Correct input.
             assertThat(
@@ -140,27 +113,20 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
     @Test
     fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(underTest.message)
 
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             runCurrent()
-            utils.deviceEntryRepository.setUnlocked(false)
-            underTest.showOrUnlockDevice()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.clearMessage()
 
             // Incomplete input.
             assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
                 .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(message).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Correct input.
             assertThat(
@@ -171,25 +137,16 @@
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(message).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun passwordAuthMethod() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(underTest.message)
             utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password
             )
             runCurrent()
-            utils.deviceEntryRepository.setUnlocked(false)
-            underTest.showOrUnlockDevice()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
-
-            underTest.clearMessage()
-            assertThat(message).isEmpty()
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
@@ -198,7 +155,6 @@
             assertThat(underTest.authenticate("alohamora".toList()))
                 .isEqualTo(AuthenticationResult.FAILED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
@@ -206,26 +162,16 @@
             // Correct input.
             assertThat(underTest.authenticate("password".toList()))
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
     @Test
     fun patternAuthMethod() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(underTest.message)
             utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern
             )
             runCurrent()
-            utils.deviceEntryRepository.setUnlocked(false)
-            underTest.showOrUnlockDevice()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
-
-            underTest.clearMessage()
-            assertThat(message).isEmpty()
-
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
@@ -241,7 +187,6 @@
             assertThat(wrongPattern.size).isAtLeast(utils.authenticationRepository.minPatternLength)
             assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
@@ -255,7 +200,6 @@
             assertThat(underTest.authenticate(tooShortPattern))
                 .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
@@ -263,51 +207,6 @@
             // Correct input.
             assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
-        }
-
-    @Test
-    fun showOrUnlockDevice_notLocked_switchesToGoneScene() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.deviceEntryRepository.setUnlocked(true)
-            runCurrent()
-
-            underTest.showOrUnlockDevice()
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
-        }
-
-    @Test
-    fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
-            utils.deviceEntryRepository.setUnlocked(false)
-
-            underTest.showOrUnlockDevice()
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
-        }
-
-    @Test
-    fun showOrUnlockDevice_customMessageShown() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            val message by collectLastValue(underTest.message)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            runCurrent()
-            utils.deviceEntryRepository.setUnlocked(false)
-
-            val customMessage = "Hello there!"
-            underTest.showOrUnlockDevice(customMessage)
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(message).isEqualTo(customMessage)
         }
 
     @Test
@@ -316,15 +215,9 @@
             val isThrottled by collectLastValue(underTest.isThrottled)
             val throttling by collectLastValue(underTest.throttling)
             val message by collectLastValue(underTest.message)
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            runCurrent()
-            underTest.showOrUnlockDevice()
-            runCurrent()
-            assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
             assertThat(isThrottled).isFalse()
             assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) { times ->
                 // Wrong PIN.
                 assertThat(underTest.authenticate(listOf(6, 7, 8, 9)))
@@ -353,7 +246,6 @@
             // Correct PIN, but throttled, so doesn't change away from the bouncer scene:
             assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
             assertTryAgainMessage(
                 message,
                 FakeAuthenticationRepository.THROTTLE_DURATION_MS.milliseconds.inWholeSeconds
@@ -379,42 +271,24 @@
                             FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING,
                     )
                 )
-            assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
 
             // Correct PIN and no longer throttled so changes to the Gone scene:
             assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(currentScene?.key).isEqualTo(SceneKey.Gone)
             assertThat(isThrottled).isFalse()
             assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
         }
 
     @Test
-    fun hide_whenOnBouncerScene_hidesBouncerAndGoesToLockscreenScene() =
+    fun imeHiddenEvent_isTriggered() =
         testScope.runTest {
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "")
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            val bouncerSceneKey = currentScene?.key
-            assertThat(bouncerSceneKey).isEqualTo(SceneKey.Bouncer)
+            val imeHiddenEvent by collectLastValue(underTest.onImeHidden)
+            runCurrent()
 
             underTest.onImeHidden()
+            runCurrent()
 
-            assertThat(currentScene?.key).isEqualTo(SceneKey.Lockscreen)
-        }
-
-    @Test
-    fun hide_whenNotOnBouncerScene_doesNothing() =
-        testScope.runTest {
-            sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "")
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            val notBouncerSceneKey = currentScene?.key
-            assertThat(notBouncerSceneKey).isNotEqualTo(SceneKey.Bouncer)
-
-            underTest.onImeHidden()
-
-            assertThat(currentScene?.key).isEqualTo(notBouncerSceneKey)
+            assertThat(imeHiddenEvent).isNotNull()
         }
 
     private fun assertTryAgainMessage(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 8e1f5ac..cfcb545 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -19,8 +19,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
@@ -37,23 +37,16 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
+    private val bouncerInteractor =
+        utils.bouncerInteractor(
+            authenticationInteractor = utils.authenticationInteractor(),
         )
     private val underTest =
         PinBouncerViewModel(
             applicationContext = context,
             viewModelScope = testScope.backgroundScope,
-            interactor =
-                utils.bouncerInteractor(
-                    deviceEntryInteractor = deviceEntryInteractor,
-                    authenticationInteractor = authenticationInteractor,
-                    sceneInteractor = sceneInteractor,
-                ),
+            interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true),
         )
 
@@ -85,18 +78,14 @@
     @Test
     fun onImeVisibilityChanged() =
         testScope.runTest {
-            val desiredScene by collectLastValue(sceneInteractor.desiredScene)
             sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "")
             sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "")
-            assertThat(desiredScene?.key).isEqualTo(SceneKey.Bouncer)
-
-            underTest.onImeVisibilityChanged(false)
-            assertThat(desiredScene?.key).isEqualTo(SceneKey.Bouncer)
+            val onImeHidden by collectLastValue(bouncerInteractor.onImeHidden)
 
             underTest.onImeVisibilityChanged(true)
-            assertThat(desiredScene?.key).isEqualTo(SceneKey.Bouncer)
+            assertThat(onImeHidden).isNull()
 
             underTest.onImeVisibilityChanged(false)
-            assertThat(desiredScene?.key).isEqualTo(SceneKey.Lockscreen)
+            assertThat(onImeHidden).isNotNull()
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 6ef518e..f4346b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -19,10 +19,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.scene.SceneTestUtils
@@ -48,16 +46,9 @@
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
     private val actionButtonInteractor = utils.bouncerActionButtonInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = utils.sceneInteractor(),
-        )
     private val bouncerInteractor =
         utils.bouncerInteractor(
-            deviceEntryInteractor = deviceEntryInteractor,
             authenticationInteractor = authenticationInteractor,
-            sceneInteractor = utils.sceneInteractor(),
         )
     private val underTest =
         utils.bouncerViewModel(
@@ -96,8 +87,7 @@
     @Test
     fun authMethodChanged_doesNotReuseInstances() =
         testScope.runTest {
-            val seen =
-                mutableMapOf<DomainLayerAuthenticationMethodModel, AuthMethodBouncerViewModel>()
+            val seen = mutableMapOf<AuthenticationMethodModel, AuthMethodBouncerViewModel>()
             val authMethodViewModel: AuthMethodBouncerViewModel? by
                 collectLastValue(underTest.authMethodViewModel)
 
@@ -137,7 +127,7 @@
     @Test
     fun authMethodsToTest_returnsCompleteSampleOfAllAuthMethodTypes() {
         assertThat(authMethodsToTest().map { it::class }.toSet())
-            .isEqualTo(DomainLayerAuthenticationMethodModel::class.sealedSubclasses.toSet())
+            .isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet())
     }
 
     @Test
@@ -145,9 +135,7 @@
         testScope.runTest {
             val message by collectLastValue(underTest.message)
             val throttling by collectLastValue(bouncerInteractor.throttling)
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(message?.isUpdateAnimated).isTrue()
 
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) {
@@ -170,9 +158,7 @@
                     }
                 )
             val throttling by collectLastValue(bouncerInteractor.throttling)
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(isInputEnabled).isTrue()
 
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) {
@@ -189,9 +175,7 @@
     fun throttlingDialogMessage() =
         testScope.runTest {
             val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
-            utils.authenticationRepository.setAuthenticationMethod(
-                DataLayerAuthenticationMethodModel.Pin
-            )
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
 
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) {
                 // Wrong PIN.
@@ -226,34 +210,29 @@
             assertThat(isSideBySideSupported).isFalse()
         }
 
-    private fun authMethodsToTest(): List<DomainLayerAuthenticationMethodModel> {
-        return listOf(
-            DomainLayerAuthenticationMethodModel.None,
-            DomainLayerAuthenticationMethodModel.Swipe,
-            DomainLayerAuthenticationMethodModel.Pin,
-            DomainLayerAuthenticationMethodModel.Password,
-            DomainLayerAuthenticationMethodModel.Pattern,
-        )
-    }
+    @Test
+    fun isFoldSplitRequired() =
+        testScope.runTest {
+            val isFoldSplitRequired by collectLastValue(underTest.isFoldSplitRequired)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            assertThat(isFoldSplitRequired).isTrue()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password
+            )
+            assertThat(isFoldSplitRequired).isFalse()
 
-    private fun FakeAuthenticationRepository.setAuthenticationMethod(
-        model: DomainLayerAuthenticationMethodModel,
-    ) {
-        setAuthenticationMethod(
-            when (model) {
-                is DomainLayerAuthenticationMethodModel.None,
-                is DomainLayerAuthenticationMethodModel.Swipe ->
-                    DataLayerAuthenticationMethodModel.None
-                is DomainLayerAuthenticationMethodModel.Pin ->
-                    DataLayerAuthenticationMethodModel.Pin
-                is DomainLayerAuthenticationMethodModel.Password ->
-                    DataLayerAuthenticationMethodModel.Password
-                is DomainLayerAuthenticationMethodModel.Pattern ->
-                    DataLayerAuthenticationMethodModel.Pattern
-            }
-        )
-        utils.deviceEntryRepository.setInsecureLockscreenEnabled(
-            model !is DomainLayerAuthenticationMethodModel.None
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pattern
+            )
+            assertThat(isFoldSplitRequired).isTrue()
+        }
+
+    private fun authMethodsToTest(): List<AuthenticationMethodModel> {
+        return listOf(
+            AuthenticationMethodModel.None,
+            AuthenticationMethodModel.Pin,
+            AuthenticationMethodModel.Password,
+            AuthenticationMethodModel.Pattern,
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 390742031..c498edf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -19,8 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
@@ -46,16 +45,9 @@
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = utils.sceneInteractor(),
-        )
     private val bouncerInteractor =
         utils.bouncerInteractor(
-            deviceEntryInteractor = deviceEntryInteractor,
             authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
         )
     private val bouncerViewModel =
         utils.bouncerViewModel(
@@ -88,8 +80,7 @@
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isEqualTo("")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(underTest.authenticationMethod)
-                .isEqualTo(DomainAuthenticationMethodModel.Password)
+            assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
         }
 
     @Test
@@ -110,19 +101,19 @@
     @Test
     fun onAuthenticateKeyPressed_whenCorrect() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            val authResult by
+                collectLastValue(authenticationInteractor.authenticationChallengeResult)
             lockDeviceAndOpenPasswordBouncer()
 
             underTest.onPasswordInputChanged("password")
             underTest.onAuthenticateKeyPressed()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+            assertThat(authResult).isTrue()
         }
 
     @Test
     fun onAuthenticateKeyPressed_whenWrong() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
@@ -132,13 +123,11 @@
 
             assertThat(password).isEqualTo("")
             assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onAuthenticateKeyPressed_whenEmpty() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             utils.authenticationRepository.setAuthenticationMethod(
@@ -147,7 +136,6 @@
             utils.deviceEntryRepository.setUnlocked(false)
             sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
             sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             // Enter nothing.
 
@@ -155,13 +143,13 @@
 
             assertThat(password).isEqualTo("")
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onAuthenticateKeyPressed_correctAfterWrong() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            val authResult by
+                collectLastValue(authenticationInteractor.authenticationChallengeResult)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
@@ -171,7 +159,7 @@
             underTest.onAuthenticateKeyPressed()
             assertThat(password).isEqualTo("")
             assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(authResult).isFalse()
 
             // Enter the correct password:
             underTest.onPasswordInputChanged("password")
@@ -179,7 +167,7 @@
 
             underTest.onAuthenticateKeyPressed()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+            assertThat(authResult).isTrue()
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 47db4f8..3f5ddba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -19,9 +19,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.res.R
@@ -49,16 +48,9 @@
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = utils.sceneInteractor(),
-        )
     private val bouncerInteractor =
         utils.bouncerInteractor(
-            deviceEntryInteractor = deviceEntryInteractor,
             authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
         )
     private val bouncerViewModel =
         utils.bouncerViewModel(
@@ -96,8 +88,7 @@
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(underTest.authenticationMethod)
-                .isEqualTo(DomainAuthenticationMethodModel.Pattern)
+            assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pattern)
         }
 
     @Test
@@ -120,7 +111,8 @@
     @Test
     fun onDragEnd_whenCorrect() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            val authResult by
+                collectLastValue(authenticationInteractor.authenticationChallengeResult)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
@@ -150,7 +142,7 @@
 
             underTest.onDragEnd()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+            assertThat(authResult).isTrue()
         }
 
     @Test
@@ -344,7 +336,8 @@
     @Test
     fun onDragEnd_correctAfterWrong() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            val authResult by
+                collectLastValue(authenticationInteractor.authenticationChallengeResult)
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
@@ -356,14 +349,14 @@
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(message?.text).isEqualTo(WRONG_PATTERN)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(authResult).isFalse()
 
             // Enter the correct pattern:
             CORRECT_PATTERN.forEach(::dragToCoordinate)
 
             underTest.onDragEnd()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+            assertThat(authResult).isTrue()
         }
 
     private fun dragOverCoordinates(vararg coordinatesDragged: Point) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 3ddac7e..6da6951 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -19,9 +19,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
@@ -48,16 +47,9 @@
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = utils.sceneInteractor(),
-        )
     private val bouncerInteractor =
         utils.bouncerInteractor(
-            deviceEntryInteractor = deviceEntryInteractor,
             authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
         )
     private val bouncerViewModel =
         utils.bouncerViewModel(
@@ -96,8 +88,7 @@
             assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN)
             assertThat(pin).isEmpty()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            assertThat(underTest.authenticationMethod)
-                .isEqualTo(DomainAuthenticationMethodModel.Pin)
+            assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
         }
 
     @Test
@@ -181,7 +172,8 @@
     @Test
     fun onAuthenticateButtonClicked_whenCorrect() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            val authResult by
+                collectLastValue(authenticationInteractor.authenticationChallengeResult)
             lockDeviceAndOpenPinBouncer()
 
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -190,7 +182,7 @@
 
             underTest.onAuthenticateButtonClicked()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+            assertThat(authResult).isTrue()
         }
 
     @Test
@@ -217,7 +209,8 @@
     @Test
     fun onAuthenticateButtonClicked_correctAfterWrong() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            val authResult by
+                collectLastValue(authenticationInteractor.authenticationChallengeResult)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
@@ -230,7 +223,7 @@
             underTest.onAuthenticateButtonClicked()
             assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
             assertThat(pin).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(authResult).isFalse()
 
             // Enter the correct PIN:
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -240,21 +233,22 @@
 
             underTest.onAuthenticateButtonClicked()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+            assertThat(authResult).isTrue()
         }
 
     @Test
     fun onAutoConfirm_whenCorrect() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAutoConfirmEnabled(true)
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
+            val authResult by
+                collectLastValue(authenticationInteractor.authenticationChallengeResult)
             lockDeviceAndOpenPinBouncer()
 
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
                 underTest.onPinButtonClicked(digit)
             }
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+            assertThat(authResult).isTrue()
         }
 
     @Test
@@ -263,7 +257,7 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAutoConfirmEnabled(true)
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
             lockDeviceAndOpenPinBouncer()
 
             FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit ->
@@ -323,7 +317,7 @@
         testScope.runTest {
             val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setAutoConfirmEnabled(true)
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
 
             assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
         }
@@ -333,7 +327,7 @@
         testScope.runTest {
             val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setAutoConfirmEnabled(true)
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
 
             underTest.onPinButtonClicked(1)
 
@@ -355,7 +349,7 @@
         testScope.runTest {
             val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setAutoConfirmEnabled(true)
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
 
             assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index ca8316d..28fae81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -21,7 +21,6 @@
 import android.appwidget.AppWidgetProviderInfo
 import android.content.BroadcastReceiver
 import android.content.ComponentName
-import android.content.pm.PackageManager
 import android.os.UserHandle
 import android.os.UserManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -34,8 +33,6 @@
 import com.android.systemui.communal.shared.CommunalWidgetHost
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.res.R
@@ -72,16 +69,12 @@
 
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
 
-    @Mock private lateinit var packageManager: PackageManager
-
     @Mock private lateinit var userManager: UserManager
 
     @Mock private lateinit var userHandle: UserHandle
 
     @Mock private lateinit var userTracker: UserTracker
 
-    @Mock private lateinit var featureFlags: FeatureFlagsClassic
-
     @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo
 
     @Mock private lateinit var providerInfoA: AppWidgetProviderInfo
@@ -113,13 +106,13 @@
         communalRepository = FakeCommunalRepository()
 
         communalEnabled(true)
-        widgetOnKeyguardEnabled(true)
         setAppWidgetIds(emptyList())
 
         overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray())
 
         whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch")
         whenever(userTracker.userHandle).thenReturn(userHandle)
+        whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap()))
     }
 
     @Test
@@ -213,7 +206,7 @@
         testScope.runTest {
             communalEnabled(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
             verifyBroadcastReceiverNeverRegistered()
         }
 
@@ -222,7 +215,7 @@
         testScope.runTest {
             userUnlocked(true)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
             verifyBroadcastReceiverNeverRegistered()
         }
 
@@ -231,7 +224,7 @@
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
             verifyBroadcastReceiverRegistered()
         }
 
@@ -241,7 +234,7 @@
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
 
-            val job = launch { repository.stopwatchAppWidgetInfo.collect() }
+            val job = launch { repository.communalWidgets.collect() }
             runCurrent()
             val receiver = broadcastReceiverUpdate()
 
@@ -252,53 +245,16 @@
         }
 
     @Test
-    fun stopwatch_whenUserUnlocks_receiveProviderInfo() =
-        testScope.runTest {
-            userUnlocked(false)
-            val repository = initCommunalWidgetRepository()
-            val lastStopwatchProviderInfo = collectLastValue(repository.stopwatchAppWidgetInfo)
-            assertThat(lastStopwatchProviderInfo()).isNull()
-
-            userUnlocked(true)
-            installedProviders(listOf(stopwatchProviderInfo))
-            broadcastReceiverUpdate()
-
-            assertThat(lastStopwatchProviderInfo()?.providerInfo).isEqualTo(stopwatchProviderInfo)
-        }
-
-    @Test
-    fun stopwatch_userUnlockedButWidgetNotInstalled_noProviderInfo() =
-        testScope.runTest {
-            userUnlocked(true)
-            installedProviders(listOf())
-
-            val repository = initCommunalWidgetRepository()
-
-            val lastStopwatchProviderInfo = collectLastValue(repository.stopwatchAppWidgetInfo)
-            assertThat(lastStopwatchProviderInfo()).isNull()
-        }
-
-    @Test
-    fun appWidgetId_providerInfoAvailable_allocateAppWidgetId() =
-        testScope.runTest {
-            userUnlocked(true)
-            installedProviders(listOf(stopwatchProviderInfo))
-            val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
-            verify(appWidgetHost).allocateAppWidgetId()
-        }
-
-    @Test
     fun appWidgetHost_userUnlocked_startListening() =
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
             verify(appWidgetHost, Mockito.never()).startListening()
 
             userUnlocked(true)
             broadcastReceiverUpdate()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
 
             verify(appWidgetHost).startListening()
         }
@@ -308,18 +264,18 @@
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
 
             userUnlocked(true)
             broadcastReceiverUpdate()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
 
             verify(appWidgetHost).startListening()
             verify(appWidgetHost, Mockito.never()).stopListening()
 
             userUnlocked(false)
             broadcastReceiverUpdate()
-            collectLastValue(repository.stopwatchAppWidgetInfo)()
+            collectLastValue(repository.communalWidgets)()
 
             verify(appWidgetHost).stopListening()
         }
@@ -334,11 +290,9 @@
             communalRepository,
             communalWidgetHost,
             communalWidgetDao,
-            packageManager,
             userManager,
             userTracker,
             logBuffer,
-            featureFlags,
         )
     }
 
@@ -385,10 +339,6 @@
         communalRepository.setIsCommunalEnabled(enabled)
     }
 
-    private fun widgetOnKeyguardEnabled(enabled: Boolean) {
-        whenever(featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)).thenReturn(enabled)
-    }
-
     private fun userUnlocked(userUnlocked: Boolean) {
         whenever(userManager.isUserUnlockingOrUnlocked(userHandle)).thenReturn(userUnlocked)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 08d54c0..a6c4f19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.coroutines.collectLastValue
@@ -45,7 +44,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 
@@ -53,8 +51,6 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 class CommunalInteractorTest : SysuiTestCase() {
-    @Mock private lateinit var stopwatchAppWidgetInfo: CommunalAppWidgetInfo
-
     private lateinit var testScope: TestScope
 
     private lateinit var tutorialRepository: FakeCommunalTutorialRepository
@@ -85,18 +81,6 @@
     }
 
     @Test
-    fun appWidgetInfoFlow() =
-        testScope.runTest {
-            val lastAppWidgetInfo = collectLastValue(underTest.appWidgetInfo)
-            runCurrent()
-            assertThat(lastAppWidgetInfo()).isNull()
-
-            widgetRepository.setStopwatchAppWidgetInfo(stopwatchAppWidgetInfo)
-            runCurrent()
-            assertThat(lastAppWidgetInfo()).isEqualTo(stopwatchAppWidgetInfo)
-        }
-
-    @Test
     fun communalEnabled() =
         testScope.runTest {
             communalRepository.setIsCommunalEnabled(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt
index 33a6667..a496292 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt
@@ -7,7 +7,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.ui.view.layout.sections.DefaultCommunalHubSection
-import com.android.systemui.communal.ui.view.layout.sections.DefaultCommunalWidgetSection
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -20,14 +19,13 @@
 @SmallTest
 class DefaultCommunalBlueprintTest : SysuiTestCase() {
     @Mock private lateinit var hubSection: DefaultCommunalHubSection
-    @Mock private lateinit var widgetSection: DefaultCommunalWidgetSection
 
     private lateinit var blueprint: DefaultCommunalBlueprint
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        blueprint = DefaultCommunalBlueprint(hubSection, widgetSection)
+        blueprint = DefaultCommunalBlueprint(hubSection)
     }
 
     @Test
@@ -35,7 +33,6 @@
         val constraintLayout = ConstraintLayout(context, null)
         blueprint.replaceViews(null, constraintLayout)
         verify(hubSection).addViews(constraintLayout)
-        verify(widgetSection).addViews(constraintLayout)
     }
 
     @Test
@@ -43,6 +40,5 @@
         val cs = ConstraintSet()
         blueprint.applyConstraints(cs)
         verify(hubSection).applyConstraints(cs)
-        verify(widgetSection).applyConstraints(cs)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
index 2c80035..97ac8c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
@@ -83,16 +83,27 @@
         }
 
     @Test
-    fun isInsecureLockscreenEnabled() =
+    fun isLockscreenEnabled() =
         testScope.runTest {
             whenever(lockPatternUtils.isLockScreenDisabled(USER_INFOS[0].id)).thenReturn(false)
             whenever(lockPatternUtils.isLockScreenDisabled(USER_INFOS[1].id)).thenReturn(true)
 
             userRepository.setSelectedUserInfo(USER_INFOS[0])
-            assertThat(underTest.isInsecureLockscreenEnabled()).isTrue()
+            assertThat(underTest.isLockscreenEnabled()).isTrue()
 
             userRepository.setSelectedUserInfo(USER_INFOS[1])
-            assertThat(underTest.isInsecureLockscreenEnabled()).isFalse()
+            assertThat(underTest.isLockscreenEnabled()).isFalse()
+        }
+
+    @Test
+    fun reportSuccessfulAuthentication_shouldUpdateIsUnlocked() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            underTest.reportSuccessfulAuthentication()
+
+            assertThat(isUnlocked).isTrue()
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index aebadc5..abd9f28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
@@ -60,7 +60,7 @@
         testScope.runTest {
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
             utils.deviceEntryRepository.apply {
-                setInsecureLockscreenEnabled(false)
+                setLockscreenEnabled(false)
 
                 // Toggle isUnlocked, twice.
                 //
@@ -83,8 +83,7 @@
     @Test
     fun isUnlocked_whenAuthMethodIsNoneAndLockscreenEnabled_isTrue() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            setupSwipeDeviceEntryMethod()
 
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             assertThat(isUnlocked).isTrue()
@@ -94,8 +93,7 @@
     fun isDeviceEntered_onLockscreenWithSwipe_isFalse() =
         testScope.runTest {
             val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            setupSwipeDeviceEntryMethod()
             switchToScene(SceneKey.Lockscreen)
 
             assertThat(isDeviceEntered).isFalse()
@@ -105,8 +103,7 @@
     fun isDeviceEntered_onShadeBeforeDismissingLockscreenWithSwipe_isFalse() =
         testScope.runTest {
             val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            setupSwipeDeviceEntryMethod()
             switchToScene(SceneKey.Lockscreen)
             runCurrent()
             switchToScene(SceneKey.Shade)
@@ -118,8 +115,7 @@
     fun isDeviceEntered_afterDismissingLockscreenWithSwipe_isTrue() =
         testScope.runTest {
             val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            setupSwipeDeviceEntryMethod()
             switchToScene(SceneKey.Lockscreen)
             runCurrent()
             switchToScene(SceneKey.Gone)
@@ -131,8 +127,7 @@
     fun isDeviceEntered_onShadeAfterDismissingLockscreenWithSwipe_isTrue() =
         testScope.runTest {
             val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            setupSwipeDeviceEntryMethod()
             switchToScene(SceneKey.Lockscreen)
             runCurrent()
             switchToScene(SceneKey.Gone)
@@ -148,7 +143,7 @@
             utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern
             )
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            utils.deviceEntryRepository.setLockscreenEnabled(true)
             switchToScene(SceneKey.Lockscreen)
             runCurrent()
             switchToScene(SceneKey.Bouncer)
@@ -160,8 +155,7 @@
     @Test
     fun canSwipeToEnter_onLockscreenWithSwipe_isTrue() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            setupSwipeDeviceEntryMethod()
             switchToScene(SceneKey.Lockscreen)
 
             val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
@@ -172,7 +166,7 @@
     fun canSwipeToEnter_onLockscreenWithPin_isFalse() =
         testScope.runTest {
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            utils.deviceEntryRepository.setLockscreenEnabled(true)
             switchToScene(SceneKey.Lockscreen)
 
             val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
@@ -182,8 +176,7 @@
     @Test
     fun canSwipeToEnter_afterLockscreenDismissedInSwipeMode_isFalse() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            setupSwipeDeviceEntryMethod()
             switchToScene(SceneKey.Lockscreen)
             runCurrent()
             switchToScene(SceneKey.Gone)
@@ -192,6 +185,11 @@
             assertThat(canSwipeToEnter).isFalse()
         }
 
+    private fun setupSwipeDeviceEntryMethod() {
+        utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+        utils.deviceEntryRepository.setLockscreenEnabled(true)
+    }
+
     @Test
     fun canSwipeToEnter_whenTrustedByTrustManager_isTrue() =
         testScope.runTest {
@@ -278,12 +276,68 @@
         }
 
     @Test
+    fun showOrUnlockDevice_notLocked_switchesToGoneScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            switchToScene(SceneKey.Lockscreen)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            utils.deviceEntryRepository.setUnlocked(true)
+            runCurrent()
+
+            underTest.attemptDeviceEntry()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            switchToScene(SceneKey.Lockscreen)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            underTest.attemptDeviceEntry()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_authMethodSwipe_switchesToGoneScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.desiredScene)
+            switchToScene(SceneKey.Lockscreen)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+
+            utils.deviceEntryRepository.setLockscreenEnabled(true)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            underTest.attemptDeviceEntry()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
     fun isBypassEnabled_disabledInRepository_false() =
         testScope.runTest {
             utils.deviceEntryRepository.setBypassEnabled(false)
             assertThat(underTest.isBypassEnabled.value).isFalse()
         }
 
+    @Test
+    fun successfulAuthenticationChallengeAttempt_updatedIsUnlockedState() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            utils.authenticationRepository.reportAuthenticationAttempt(true)
+
+            assertThat(isUnlocked).isTrue()
+        }
+
     private fun switchToScene(sceneKey: SceneKey) {
         sceneInteractor.changeScene(SceneModel(sceneKey), "reason")
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 7de28de..0b3bc9d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
@@ -45,7 +45,7 @@
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            utils.deviceEntryRepository.setLockscreenEnabled(true)
             utils.deviceEntryRepository.setUnlocked(true)
             sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason")
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
index b5e7e2c..92c2d74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
@@ -130,7 +130,7 @@
                     "sd=null, " +
                     "svi=None, " +
                     "enabled=ENABLED, " +
-                    "a11y=null" +
+                    "a11y=android.widget.Switch" +
                     "], " +
                     "data=test_data"
             )
@@ -154,7 +154,7 @@
                     "sd=null, " +
                     "svi=None, " +
                     "enabled=ENABLED, " +
-                    "a11y=null], " +
+                    "a11y=android.widget.Switch], " +
                     "data=test_data"
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index a9f8239..d582b9e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
@@ -90,13 +90,8 @@
 
         underTest =
             QuickSettingsSceneViewModel(
-                bouncerInteractor =
-                    utils.bouncerInteractor(
-                        deviceEntryInteractor =
-                            utils.deviceEntryInteractor(
-                                authenticationInteractor = authenticationInteractor,
-                                sceneInteractor = sceneInteractor,
-                            ),
+                deviceEntryInteractor =
+                    utils.deviceEntryInteractor(
                         authenticationInteractor = authenticationInteractor,
                         sceneInteractor = sceneInteractor,
                     ),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index e84d274..d1db9c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -26,7 +26,7 @@
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
@@ -39,7 +39,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
-import com.android.systemui.scene.SceneTestUtils.Companion.toDataLayer
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
@@ -60,6 +59,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import junit.framework.Assert.assertTrue
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -72,7 +72,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -135,9 +134,7 @@
 
     private val bouncerInteractor =
         utils.bouncerInteractor(
-            deviceEntryInteractor = deviceEntryInteractor,
             authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
         )
 
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
@@ -234,7 +231,6 @@
             ShadeSceneViewModel(
                 applicationScope = testScope.backgroundScope,
                 deviceEntryInteractor = deviceEntryInteractor,
-                bouncerInteractor = bouncerInteractor,
                 shadeHeaderViewModel = shadeHeaderViewModel,
             )
 
@@ -247,7 +243,6 @@
                 applicationScope = testScope.backgroundScope,
                 sceneInteractor = sceneInteractor,
                 deviceEntryInteractor = deviceEntryInteractor,
-                authenticationInteractor = authenticationInteractor,
                 keyguardInteractor = keyguardInteractor,
                 flags = utils.sceneContainerFlags,
                 sysUiState = sysUiState,
@@ -255,6 +250,7 @@
                 sceneLogger = mock(),
                 falsingCollector = utils.falsingCollector(),
                 powerInteractor = powerInteractor,
+                bouncerInteractor = bouncerInteractor,
             )
         startable.start()
 
@@ -298,7 +294,7 @@
     @Test
     fun swipeUpOnLockscreen_withAuthMethodSwipe_dismissesLockscreen() =
         testScope.runTest {
-            setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe)
+            setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
 
             val upDestinationSceneKey by
                 collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
@@ -312,7 +308,7 @@
     fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
         testScope.runTest {
             val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey)
-            setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe)
+            setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
             assertCurrentScene(SceneKey.Lockscreen)
 
             // Emulate a user swipe to the shade scene.
@@ -329,7 +325,8 @@
     fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() =
         testScope.runTest {
             val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey)
-            setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe)
+            setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
+            assertTrue(deviceEntryInteractor.canSwipeToEnter.value)
             assertCurrentScene(SceneKey.Lockscreen)
 
             // Emulate a user swipe to dismiss the lockscreen.
@@ -349,7 +346,7 @@
     @Test
     fun withAuthMethodNone_deviceWakeUp_skipsLockscreen() =
         testScope.runTest {
-            setAuthMethod(DomainLayerAuthenticationMethodModel.None)
+            setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = false)
             putDeviceToSleep(instantlyLockDevice = false)
             assertCurrentScene(SceneKey.Lockscreen)
 
@@ -360,7 +357,7 @@
     @Test
     fun withAuthMethodSwipe_deviceWakeUp_doesNotSkipLockscreen() =
         testScope.runTest {
-            setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe)
+            setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
             putDeviceToSleep(instantlyLockDevice = false)
             assertCurrentScene(SceneKey.Lockscreen)
 
@@ -428,7 +425,7 @@
     @Test
     fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() =
         testScope.runTest {
-            setAuthMethod(DomainLayerAuthenticationMethodModel.Password)
+            setAuthMethod(AuthenticationMethodModel.Password)
             val upDestinationSceneKey by
                 collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
             assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Bouncer)
@@ -445,7 +442,7 @@
     @Test
     fun bouncerActionButtonClick_opensEmergencyServicesDialer() =
         testScope.runTest {
-            setAuthMethod(DomainLayerAuthenticationMethodModel.Password)
+            setAuthMethod(AuthenticationMethodModel.Password)
             val upDestinationSceneKey by
                 collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
             assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Bouncer)
@@ -464,7 +461,7 @@
     @Test
     fun bouncerActionButtonClick_duringCall_returnsToCall() =
         testScope.runTest {
-            setAuthMethod(DomainLayerAuthenticationMethodModel.Password)
+            setAuthMethod(AuthenticationMethodModel.Password)
             startPhoneCall()
             val upDestinationSceneKey by
                 collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
@@ -508,19 +505,19 @@
 
     /** Updates the current authentication method and related states in the data layer. */
     private fun TestScope.setAuthMethod(
-        authMethod: DomainLayerAuthenticationMethodModel,
+        authMethod: AuthenticationMethodModel,
+        enableLockscreen: Boolean = true
     ) {
+        if (authMethod.isSecure) {
+            assert(enableLockscreen) {
+                "Lockscreen cannot be disabled with a secure authentication method."
+            }
+        }
         // Set the lockscreen enabled bit _before_ set the auth method as the code picks up on the
         // lockscreen enabled bit _after_ the auth method is changed and the lockscreen enabled bit
         // is not an observable that can trigger a new evaluation.
-        utils.deviceEntryRepository.setInsecureLockscreenEnabled(
-            authMethod !is DomainLayerAuthenticationMethodModel.None
-        )
-        utils.authenticationRepository.setAuthenticationMethod(authMethod.toDataLayer())
-        if (!authMethod.isSecure) {
-            // When the auth method is not secure, the device is never considered locked.
-            utils.deviceEntryRepository.setUnlocked(true)
-        }
+        utils.deviceEntryRepository.setLockscreenEnabled(enableLockscreen)
+        utils.authenticationRepository.setAuthenticationMethod(authMethod)
         runCurrent()
     }
 
@@ -648,6 +645,9 @@
 
         emulateUserDrivenTransition(SceneKey.Bouncer)
         enterPin()
+        // This repository state is not changed by the AuthInteractor, it relies on
+        // KeyguardStateController.
+        utils.deviceEntryRepository.setUnlocked(true)
         emulateUiSceneTransition(
             expectedVisible = false,
         )
@@ -707,7 +707,7 @@
     }
 
     /** Emulates the dismissal of the IME (soft keyboard). */
-    private fun TestScope.dismissIme(
+    private suspend fun TestScope.dismissIme(
         showImeBeforeDismissing: Boolean = true,
     ) {
         bouncerViewModel.authMethodViewModel.value?.apply {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index f6362fe..2f654e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -24,16 +24,15 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags as AconfigFlags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.model.SysUiState
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.scene.SceneTestUtils
-import com.android.systemui.scene.SceneTestUtils.Companion.toDataLayer
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -63,8 +62,12 @@
     private val sceneInteractor = utils.sceneInteractor()
     private val sceneContainerFlags = utils.sceneContainerFlags
     private val authenticationInteractor = utils.authenticationInteractor()
+    private val bouncerInteractor =
+        utils.bouncerInteractor(authenticationInteractor = authenticationInteractor)
+    private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
     private val deviceEntryInteractor =
         utils.deviceEntryInteractor(
+            faceAuthRepository = faceAuthRepository,
             authenticationInteractor = authenticationInteractor,
             sceneInteractor = sceneInteractor,
         )
@@ -78,7 +81,6 @@
             applicationScope = testScope.backgroundScope,
             sceneInteractor = sceneInteractor,
             deviceEntryInteractor = deviceEntryInteractor,
-            authenticationInteractor = authenticationInteractor,
             keyguardInteractor = keyguardInteractor,
             flags = sceneContainerFlags,
             sysUiState = sysUiState,
@@ -86,6 +88,7 @@
             sceneLogger = mock(),
             falsingCollector = falsingCollector,
             powerInteractor = powerInteractor,
+            bouncerInteractor = bouncerInteractor,
         )
 
     @Before
@@ -198,12 +201,55 @@
             assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
             underTest.start()
 
+            // Authenticate using a passive auth method like face auth while bypass is disabled.
+            faceAuthRepository.isAuthenticated.value = true
             utils.deviceEntryRepository.setUnlocked(true)
 
             assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
         }
 
     @Test
+    fun stayOnCurrentSceneWhenDeviceIsUnlockedAndUserIsNotOnLockscreen() =
+        testScope.runTest {
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+            val transitionStateFlowValue =
+                prepareState(
+                    isBypassEnabled = true,
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+                    initialSceneKey = SceneKey.Lockscreen,
+                )
+            underTest.start()
+            runCurrent()
+
+            sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "switch to shade")
+            transitionStateFlowValue.value = ObservableTransitionState.Idle(SceneKey.Shade)
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+
+            utils.deviceEntryRepository.setUnlocked(true)
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+        }
+
+    @Test
+    fun switchToGoneWhenDeviceIsUnlockedAndUserIsOnBouncerWithBypassDisabled() =
+        testScope.runTest {
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+            prepareState(
+                isBypassEnabled = false,
+                initialSceneKey = SceneKey.Bouncer,
+            )
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer)
+            underTest.start()
+
+            // Authenticate using a passive auth method like face auth while bypass is disabled.
+            faceAuthRepository.isAuthenticated.value = true
+            utils.deviceEntryRepository.setUnlocked(true)
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
     fun switchToLockscreenWhenDeviceSleepsLocked() =
         testScope.runTest {
             val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
@@ -255,6 +301,7 @@
             prepareState(
                 initialSceneKey = SceneKey.Lockscreen,
                 authenticationMethod = AuthenticationMethodModel.None,
+                isLockscreenEnabled = false,
             )
             assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
             underTest.start()
@@ -269,7 +316,8 @@
             val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
             prepareState(
                 initialSceneKey = SceneKey.Lockscreen,
-                authenticationMethod = AuthenticationMethodModel.Swipe,
+                authenticationMethod = AuthenticationMethodModel.None,
+                isLockscreenEnabled = true,
             )
             assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
             underTest.start()
@@ -406,6 +454,24 @@
         }
 
     @Test
+    fun bouncerImeHidden_shouldTransitionBackToLockscreen() =
+        testScope.runTest {
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+            prepareState(
+                initialSceneKey = SceneKey.Lockscreen,
+                authenticationMethod = AuthenticationMethodModel.Password,
+                isDeviceUnlocked = false,
+            )
+            underTest.start()
+            runCurrent()
+
+            bouncerInteractor.onImeHidden()
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
+        }
+
+    @Test
     fun collectFalsingSignals_screenOnAndOff_aodUnavailable() =
         testScope.runTest {
             utils.keyguardRepository.setAodAvailable(false)
@@ -526,8 +592,14 @@
         isBypassEnabled: Boolean = false,
         initialSceneKey: SceneKey? = null,
         authenticationMethod: AuthenticationMethodModel? = null,
+        isLockscreenEnabled: Boolean = true,
         startsAwake: Boolean = true,
     ): MutableStateFlow<ObservableTransitionState> {
+        if (authenticationMethod?.isSecure == true) {
+            assert(isLockscreenEnabled) {
+                "Lockscreen cannot be disabled while having a secure authentication method"
+            }
+        }
         sceneContainerFlags.enabled = true
         utils.deviceEntryRepository.setUnlocked(isDeviceUnlocked)
         utils.deviceEntryRepository.setBypassEnabled(isBypassEnabled)
@@ -542,11 +614,9 @@
             sceneInteractor.onSceneChanged(SceneModel(it), "reason")
         }
         authenticationMethod?.let {
-            utils.authenticationRepository.setAuthenticationMethod(
-                authenticationMethod.toDataLayer()
-            )
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(
-                authenticationMethod != AuthenticationMethodModel.None
+            utils.authenticationRepository.setAuthenticationMethod(authenticationMethod)
+            utils.deviceEntryRepository.setLockscreenEnabled(
+                isLockscreenEnabled = isLockscreenEnabled
             )
         }
         if (startsAwake) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
index e920687..20b19fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
@@ -158,6 +158,15 @@
         }
 
     @Test
+    fun updateLegacyLockscreenShadeTracking() =
+        testScope.runTest {
+            assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(false)
+
+            underTest.setLegacyLockscreenShadeTracking(true)
+            assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(true)
+        }
+
+    @Test
     fun updateLegacyQsTracking() =
         testScope.runTest {
             assertThat(underTest.legacyQsTracking.value).isEqualTo(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 589f9ae..fa849fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
@@ -96,12 +96,6 @@
             ShadeSceneViewModel(
                 applicationScope = testScope.backgroundScope,
                 deviceEntryInteractor = deviceEntryInteractor,
-                bouncerInteractor =
-                    utils.bouncerInteractor(
-                        deviceEntryInteractor = deviceEntryInteractor,
-                        authenticationInteractor = authenticationInteractor,
-                        sceneInteractor = sceneInteractor,
-                    ),
                 shadeHeaderViewModel = shadeHeaderViewModel,
             )
     }
@@ -130,7 +124,7 @@
     fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            utils.deviceEntryRepository.setLockscreenEnabled(true)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
             sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason")
             sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason")
@@ -142,7 +136,7 @@
     fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
-            utils.deviceEntryRepository.setInsecureLockscreenEnabled(true)
+            utils.deviceEntryRepository.setLockscreenEnabled(true)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
             sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason")
             sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index a1425f8..ae32142 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -323,6 +323,20 @@
     }
 
     @Test
+    public void testCurrentUserPrivateNotificationsNullChannel() {
+        // GIVEN current user allows private notifications to show
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+                mCurrentUser.id);
+        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+
+        mCurrentUserNotif.setRanking(new RankingBuilder(mCurrentUserNotif.getRanking())
+                .setChannel(null)
+                .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
+        // THEN the notification is not redacted
+        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+    }
+
+    @Test
     public void testWorkPrivateNotificationsRedacted() {
         // GIVEN work profile doesn't private notifications to show
         mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index 80d941a..722b170 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -32,9 +32,11 @@
         VisualInterruptionDecisionProviderImpl(
             ambientDisplayConfiguration,
             batteryController,
+            deviceProvisionedController,
             globalSettings,
             headsUpManager,
             keyguardNotificationVisibilityProvider,
+            keyguardStateController,
             logger,
             mainHandler,
             powerManager,
@@ -50,6 +52,7 @@
             assertPeekNotSuppressed()
             assertPulseNotSuppressed()
             assertBubbleNotSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -59,6 +62,7 @@
             assertPeekNotSuppressed()
             assertPulseNotSuppressed()
             assertBubbleNotSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -68,6 +72,7 @@
             assertPeekSuppressed()
             assertPulseNotSuppressed()
             assertBubbleNotSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -77,6 +82,7 @@
             assertPeekSuppressed()
             assertPulseNotSuppressed()
             assertBubbleNotSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -86,6 +92,7 @@
             assertPeekNotSuppressed()
             assertPulseSuppressed()
             assertBubbleNotSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -95,6 +102,7 @@
             assertPeekNotSuppressed()
             assertPulseSuppressed()
             assertBubbleNotSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -104,6 +112,7 @@
             assertPeekNotSuppressed()
             assertPulseNotSuppressed()
             assertBubbleSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -113,6 +122,7 @@
             assertPeekNotSuppressed()
             assertPulseNotSuppressed()
             assertBubbleSuppressed()
+            assertFsiNotSuppressed()
         }
     }
 
@@ -193,6 +203,10 @@
         assertShouldBubble(buildBubbleEntry())
     }
 
+    private fun assertFsiNotSuppressed() {
+        forEachFsiState { assertShouldFsi(buildFsiEntry()) }
+    }
+
     private fun withCondition(condition: VisualInterruptionCondition, block: () -> Unit) {
         provider.addCondition(condition)
         block()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index 7f12b22..0f29836 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -20,6 +20,7 @@
 import android.app.Notification
 import android.app.Notification.BubbleMetadata
 import android.app.Notification.FLAG_BUBBLE
+import android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED
 import android.app.Notification.GROUP_ALERT_ALL
 import android.app.Notification.GROUP_ALERT_CHILDREN
 import android.app.Notification.GROUP_ALERT_SUMMARY
@@ -29,6 +30,7 @@
 import android.app.NotificationManager.IMPORTANCE_HIGH
 import android.app.NotificationManager.IMPORTANCE_LOW
 import android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT
+import android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
 import android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK
 import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE
 import android.app.PendingIntent
@@ -56,18 +58,19 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS
-import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.FakeDeviceProvisionedController
 import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.FakeGlobalSettings
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.utils.leaks.FakeBatteryController
+import com.android.systemui.utils.leaks.FakeKeyguardStateController
 import com.android.systemui.utils.leaks.LeakCheckedTest
 import com.android.systemui.utils.os.FakeHandler
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mockito.`when` as whenever
@@ -77,13 +80,13 @@
 
     protected val ambientDisplayConfiguration = FakeAmbientDisplayConfiguration(context)
     protected val batteryController = FakeBatteryController(leakCheck)
-    protected val deviceProvisionedController: DeviceProvisionedController = mock()
+    protected val deviceProvisionedController = FakeDeviceProvisionedController()
     protected val flags: NotifPipelineFlags = mock()
     protected val globalSettings = FakeGlobalSettings()
     protected val headsUpManager: HeadsUpManager = mock()
     protected val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider =
         mock()
-    protected val keyguardStateController: KeyguardStateController = mock()
+    protected val keyguardStateController = FakeKeyguardStateController(leakCheck)
     protected val logger: NotificationInterruptLogger = mock()
     protected val mainHandler = FakeHandler(Looper.getMainLooper())
     protected val powerManager: PowerManager = mock()
@@ -137,15 +140,18 @@
     }
 
     @Test
-    fun testShouldNotPeek_packageSnoozed() {
+    fun testShouldNotPeek_packageSnoozed_withoutFsi() {
         ensurePeekState { hunSnoozed = true }
         assertShouldNotHeadsUp(buildPeekEntry())
     }
 
     @Test
-    fun testShouldPeek_packageSnoozedButFsi() {
-        ensurePeekState { hunSnoozed = true }
-        assertShouldHeadsUp(buildFsiEntry())
+    fun testShouldPeek_packageSnoozed_withFsi() {
+        val entry = buildFsiEntry()
+        forEachPeekableFsiState {
+            ensurePeekState { hunSnoozed = true }
+            assertShouldHeadsUp(entry)
+        }
     }
 
     @Test
@@ -217,29 +223,31 @@
     @Test
     fun testShouldPeek_defaultLegacySuppressor() {
         ensurePeekState()
-        provider.addLegacySuppressor(neverSuppresses)
-        assertShouldHeadsUp(buildPeekEntry())
+        withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPeekEntry()) }
     }
 
     @Test
     fun testShouldNotPeek_legacySuppressInterruptions() {
         ensurePeekState()
-        provider.addLegacySuppressor(alwaysSuppressesInterruptions)
-        assertShouldNotHeadsUp(buildPeekEntry())
+        withLegacySuppressor(alwaysSuppressesInterruptions) {
+            assertShouldNotHeadsUp(buildPeekEntry())
+        }
     }
 
     @Test
     fun testShouldNotPeek_legacySuppressAwakeInterruptions() {
         ensurePeekState()
-        provider.addLegacySuppressor(alwaysSuppressesAwakeInterruptions)
-        assertShouldNotHeadsUp(buildPeekEntry())
+        withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
+            assertShouldNotHeadsUp(buildPeekEntry())
+        }
     }
 
     @Test
     fun testShouldNotPeek_legacySuppressAwakeHeadsUp() {
         ensurePeekState()
-        provider.addLegacySuppressor(alwaysSuppressesAwakeHeadsUp)
-        assertShouldNotHeadsUp(buildPeekEntry())
+        withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
+            assertShouldNotHeadsUp(buildPeekEntry())
+        }
     }
 
     @Test
@@ -251,29 +259,31 @@
     @Test
     fun testShouldPulse_defaultLegacySuppressor() {
         ensurePulseState()
-        provider.addLegacySuppressor(neverSuppresses)
-        assertShouldHeadsUp(buildPulseEntry())
+        withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPulseEntry()) }
     }
 
     @Test
     fun testShouldNotPulse_legacySuppressInterruptions() {
         ensurePulseState()
-        provider.addLegacySuppressor(alwaysSuppressesInterruptions)
-        assertShouldNotHeadsUp(buildPulseEntry())
+        withLegacySuppressor(alwaysSuppressesInterruptions) {
+            assertShouldNotHeadsUp(buildPulseEntry())
+        }
     }
 
     @Test
     fun testShouldPulse_legacySuppressAwakeInterruptions() {
         ensurePulseState()
-        provider.addLegacySuppressor(alwaysSuppressesAwakeInterruptions)
-        assertShouldHeadsUp(buildPulseEntry())
+        withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
+            assertShouldHeadsUp(buildPulseEntry())
+        }
     }
 
     @Test
     fun testShouldPulse_legacySuppressAwakeHeadsUp() {
         ensurePulseState()
-        provider.addLegacySuppressor(alwaysSuppressesAwakeHeadsUp)
-        assertShouldHeadsUp(buildPulseEntry())
+        withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
+            assertShouldHeadsUp(buildPulseEntry())
+        }
     }
 
     @Test
@@ -439,29 +449,31 @@
     @Test
     fun testShouldBubble_defaultLegacySuppressor() {
         ensureBubbleState()
-        provider.addLegacySuppressor(neverSuppresses)
-        assertShouldBubble(buildBubbleEntry())
+        withLegacySuppressor(neverSuppresses) { assertShouldBubble(buildBubbleEntry()) }
     }
 
     @Test
     fun testShouldNotBubble_legacySuppressInterruptions() {
         ensureBubbleState()
-        provider.addLegacySuppressor(alwaysSuppressesInterruptions)
-        assertShouldNotBubble(buildBubbleEntry())
+        withLegacySuppressor(alwaysSuppressesInterruptions) {
+            assertShouldNotBubble(buildBubbleEntry())
+        }
     }
 
     @Test
     fun testShouldNotBubble_legacySuppressAwakeInterruptions() {
         ensureBubbleState()
-        provider.addLegacySuppressor(alwaysSuppressesAwakeInterruptions)
-        assertShouldNotBubble(buildBubbleEntry())
+        withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
+            assertShouldNotBubble(buildBubbleEntry())
+        }
     }
 
     @Test
     fun testShouldBubble_legacySuppressAwakeHeadsUp() {
         ensureBubbleState()
-        provider.addLegacySuppressor(alwaysSuppressesAwakeHeadsUp)
-        assertShouldBubble(buildBubbleEntry())
+        withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
+            assertShouldBubble(buildBubbleEntry())
+        }
     }
 
     @Test
@@ -477,6 +489,107 @@
     }
 
     @Test
+    fun testShouldNotFsi_noFullScreenIntent() {
+        forEachFsiState { assertShouldNotFsi(buildFsiEntry { hasFsi = false }) }
+    }
+
+    @Test
+    fun testShouldNotFsi_showStickyHun() {
+        forEachFsiState {
+            assertShouldNotFsi(
+                buildFsiEntry {
+                    hasFsi = false
+                    isStickyAndNotDemoted = true
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testShouldNotFsi_onlyDnd() {
+        forEachFsiState {
+            assertShouldNotFsi(
+                buildFsiEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_FULL_SCREEN_INTENT },
+                expectWouldInterruptWithoutDnd = true
+            )
+        }
+    }
+
+    @Test
+    fun testShouldNotFsi_notImportantEnough() {
+        forEachFsiState { assertShouldNotFsi(buildFsiEntry { importance = IMPORTANCE_DEFAULT }) }
+    }
+
+    @Test
+    fun testShouldNotFsi_notOnlyDnd() {
+        forEachFsiState {
+            assertShouldNotFsi(
+                buildFsiEntry {
+                    suppressedVisualEffects = SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
+                    importance = IMPORTANCE_DEFAULT
+                },
+                expectWouldInterruptWithoutDnd = false
+            )
+        }
+    }
+
+    @Test
+    fun testShouldNotFsi_suppressiveGroupAlertBehavior() {
+        forEachFsiState {
+            assertShouldNotFsi(
+                buildFsiEntry {
+                    isGrouped = true
+                    isGroupSummary = true
+                    groupAlertBehavior = GROUP_ALERT_CHILDREN
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testShouldFsi_suppressiveGroupAlertBehavior_notGrouped() {
+        forEachFsiState {
+            assertShouldFsi(
+                buildFsiEntry {
+                    isGrouped = false
+                    isGroupSummary = true
+                    groupAlertBehavior = GROUP_ALERT_CHILDREN
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testShouldFsi_suppressiveGroupAlertBehavior_notSuppressive() {
+        forEachFsiState {
+            assertShouldFsi(
+                buildFsiEntry {
+                    isGrouped = true
+                    isGroupSummary = true
+                    groupAlertBehavior = GROUP_ALERT_ALL
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testShouldNotFsi_suppressiveBubbleMetadata() {
+        forEachFsiState {
+            assertShouldNotFsi(
+                buildFsiEntry {
+                    hasBubbleMetadata = true
+                    bubbleSuppressesNotification = true
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testShouldNotFsi_packageSuspended() {
+        forEachFsiState { assertShouldNotFsi(buildFsiEntry { packageSuspended = true }) }
+    }
+
+    @Test
     fun testShouldFsi_notInteractive() {
         ensureNotInteractiveFsiState()
         assertShouldFsi(buildFsiEntry())
@@ -494,6 +607,76 @@
         assertShouldFsi(buildFsiEntry())
     }
 
+    @Test
+    fun testShouldNotFsi_expectedToHun() {
+        forEachPeekableFsiState {
+            ensurePeekState()
+            assertShouldNotFsi(buildFsiEntry())
+        }
+    }
+
+    @Test
+    fun testShouldNotFsi_expectedToHun_hunSnoozed() {
+        forEachPeekableFsiState {
+            ensurePeekState { hunSnoozed = true }
+            assertShouldNotFsi(buildFsiEntry())
+        }
+    }
+
+    @Test
+    fun testShouldFsi_lockedShade() {
+        ensureLockedShadeFsiState()
+        assertShouldFsi(buildFsiEntry())
+    }
+
+    @Test
+    fun testShouldFsi_keyguardOccluded() {
+        ensureKeyguardOccludedFsiState()
+        assertShouldFsi(buildFsiEntry())
+    }
+
+    @Test
+    fun testShouldFsi_deviceNotProvisioned() {
+        ensureDeviceNotProvisionedFsiState()
+        assertShouldFsi(buildFsiEntry())
+    }
+
+    @Test
+    fun testShouldNotFsi_noHunOrKeyguard() {
+        ensureNoHunOrKeyguardFsiState()
+        assertShouldNotFsi(buildFsiEntry())
+    }
+
+    @Test
+    fun testShouldFsi_defaultLegacySuppressor() {
+        forEachFsiState {
+            withLegacySuppressor(neverSuppresses) { assertShouldFsi(buildFsiEntry()) }
+        }
+    }
+
+    @Test
+    fun testShouldFsi_suppressInterruptions() {
+        forEachFsiState {
+            withLegacySuppressor(alwaysSuppressesInterruptions) { assertShouldFsi(buildFsiEntry()) }
+        }
+    }
+
+    @Test
+    fun testShouldFsi_suppressAwakeInterruptions() {
+        forEachFsiState {
+            withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
+                assertShouldFsi(buildFsiEntry())
+            }
+        }
+    }
+
+    @Test
+    fun testShouldFsi_suppressAwakeHeadsUp() {
+        forEachFsiState {
+            withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) { assertShouldFsi(buildFsiEntry()) }
+        }
+    }
+
     protected data class State(
         var hunSettingEnabled: Boolean? = null,
         var hunSnoozed: Boolean? = null,
@@ -505,6 +688,9 @@
         var keyguardShouldHideNotification: Boolean? = null,
         var pulseOnNotificationsEnabled: Boolean? = null,
         var statusBarState: Int? = null,
+        var keyguardIsShowing: Boolean = false,
+        var keyguardIsOccluded: Boolean = false,
+        var deviceProvisioned: Boolean = true
     )
 
     protected fun setState(state: State): Unit =
@@ -536,6 +722,11 @@
             }
 
             statusBarState?.let { statusBarStateController.state = it }
+
+            keyguardStateController.isOccluded = keyguardIsOccluded
+            keyguardStateController.isShowing = keyguardIsShowing
+
+            deviceProvisionedController.deviceProvisioned = deviceProvisioned
         }
 
     protected fun ensureState(block: State.() -> Unit) =
@@ -565,26 +756,104 @@
     protected fun ensureBubbleState(block: State.() -> Unit = {}) = ensureState(block)
 
     protected fun ensureNotInteractiveFsiState(block: State.() -> Unit = {}) = ensureState {
-        isDreaming = false
         isInteractive = false
-        statusBarState = SHADE
         run(block)
     }
 
     protected fun ensureDreamingFsiState(block: State.() -> Unit = {}) = ensureState {
-        isDreaming = true
         isInteractive = true
-        statusBarState = SHADE
+        isDreaming = true
         run(block)
     }
 
     protected fun ensureKeyguardFsiState(block: State.() -> Unit = {}) = ensureState {
-        isDreaming = false
         isInteractive = true
+        isDreaming = false
         statusBarState = KEYGUARD
         run(block)
     }
 
+    protected fun ensureLockedShadeFsiState(block: State.() -> Unit = {}) = ensureState {
+        // It is assumed *but not checked in the code* that statusBarState is SHADE_LOCKED.
+        isInteractive = true
+        isDreaming = false
+        statusBarState = SHADE
+        hunSettingEnabled = false
+        keyguardIsShowing = true
+        keyguardIsOccluded = false
+        run(block)
+    }
+
+    protected fun ensureKeyguardOccludedFsiState(block: State.() -> Unit = {}) = ensureState {
+        isInteractive = true
+        isDreaming = false
+        statusBarState = SHADE
+        hunSettingEnabled = false
+        keyguardIsShowing = true
+        keyguardIsOccluded = true
+        run(block)
+    }
+
+    protected fun ensureDeviceNotProvisionedFsiState(block: State.() -> Unit = {}) = ensureState {
+        isInteractive = true
+        isDreaming = false
+        statusBarState = SHADE
+        hunSettingEnabled = false
+        keyguardIsShowing = false
+        deviceProvisioned = false
+        run(block)
+    }
+
+    protected fun ensureNoHunOrKeyguardFsiState(block: State.() -> Unit = {}) = ensureState {
+        isInteractive = true
+        isDreaming = false
+        statusBarState = SHADE
+        hunSettingEnabled = false
+        keyguardIsShowing = false
+        deviceProvisioned = true
+        run(block)
+    }
+
+    protected fun forEachFsiState(block: () -> Unit) {
+        ensureNotInteractiveFsiState()
+        block()
+
+        ensureDreamingFsiState()
+        block()
+
+        ensureKeyguardFsiState()
+        block()
+
+        ensureLockedShadeFsiState()
+        block()
+
+        ensureKeyguardOccludedFsiState()
+        block()
+
+        ensureDeviceNotProvisionedFsiState()
+        block()
+    }
+
+    private fun forEachPeekableFsiState(extendState: State.() -> Unit = {}, block: () -> Unit) {
+        ensureLockedShadeFsiState(extendState)
+        block()
+
+        ensureKeyguardOccludedFsiState(extendState)
+        block()
+
+        ensureDeviceNotProvisionedFsiState(extendState)
+        block()
+    }
+
+    protected fun withLegacySuppressor(
+        suppressor: NotificationInterruptSuppressor,
+        block: () -> Unit
+    ) {
+        provider.addLegacySuppressor(suppressor)
+        block()
+        provider.removeLegacySuppressor(suppressor)
+    }
+
     protected fun assertShouldHeadsUp(entry: NotificationEntry) =
         provider.makeUnloggedHeadsUpDecision(entry).let {
             assertTrue("unexpected suppressed HUN: ${it.logReason}", it.shouldInterrupt)
@@ -610,9 +879,19 @@
             assertTrue("unexpected suppressed FSI: ${it.logReason}", it.shouldInterrupt)
         }
 
-    protected fun assertShouldNotFsi(entry: NotificationEntry) =
+    protected fun assertShouldNotFsi(
+        entry: NotificationEntry,
+        expectWouldInterruptWithoutDnd: Boolean? = null
+    ) =
         provider.makeUnloggedFullScreenIntentDecision(entry).let {
             assertFalse("unexpected unsuppressed FSI: ${it.logReason}", it.shouldInterrupt)
+            if (expectWouldInterruptWithoutDnd != null) {
+                assertEquals(
+                    "unexpected unsuppressed-without-DND FSI: ${it.logReason}",
+                    expectWouldInterruptWithoutDnd,
+                    it.wouldInterruptWithoutDnd
+                )
+            }
         }
 
     protected class EntryBuilder(val context: Context) {
@@ -630,6 +909,8 @@
         var isGroupSummary: Boolean? = null
         var groupAlertBehavior: Int? = null
         var hasJustLaunchedFsi = false
+        var isStickyAndNotDemoted = false
+        var packageSuspended: Boolean? = null
 
         private fun buildBubbleMetadata(): BubbleMetadata {
             val builder =
@@ -681,6 +962,10 @@
                     if (isBubble) {
                         flags = flags or FLAG_BUBBLE
                     }
+
+                    if (isStickyAndNotDemoted) {
+                        flags = flags or FLAG_FSI_REQUESTED_BUT_DENIED
+                    }
                 }
                 .let { NotificationEntryBuilder().setNotification(it) }
                 .apply {
@@ -699,10 +984,15 @@
                         it.notifyFullScreenIntentLaunched()
                     }
 
+                    if (isStickyAndNotDemoted) {
+                        assertFalse(it.isDemoted)
+                    }
+
                     modifyRanking(it)
                         .apply {
                             suppressedVisualEffects?.let { setSuppressedVisualEffects(it) }
                             visibilityOverride?.let { setVisibilityOverride(it) }
+                            packageSuspended?.let { setSuspended(it) }
                         }
                         .build()
                 }
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 6203531..e91d6d7 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
@@ -83,6 +83,7 @@
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -906,6 +907,20 @@
         assertEquals(bottomImeInset, mStackScrollerInternal.mBottomInset);
     }
 
+    @Test
+    public void testSetMaxDisplayedNotifications_notifiesListeners() {
+        ExpandableView.OnHeightChangedListener listener =
+                mock(ExpandableView.OnHeightChangedListener.class);
+        Runnable runnable = mock(Runnable.class);
+        mStackScroller.setOnHeightChangedListener(listener);
+        mStackScroller.setOnHeightChangedRunnable(runnable);
+
+        mStackScroller.setMaxDisplayedNotifications(50);
+
+        verify(listener).onHeightChanged(mNotificationShelf, false);
+        verify(runnable).run();
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 22553df..db8f217 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -40,13 +40,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.user.domain.UserDomainLayerModule
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
@@ -84,25 +79,13 @@
         }
     }
 
-    private val notificationStackSizeCalculator: NotificationStackSizeCalculator = mock()
-    private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController =
-        mock {
-            whenever(view).thenReturn(mock())
-            whenever(shelfHeight).thenReturn(0)
-        }
-
     private val testComponent: TestComponent =
         DaggerSharedNotificationContainerViewModelTest_TestComponent.factory()
             .create(
                 test = this,
                 featureFlags =
                     FakeFeatureFlagsClassicModule { set(Flags.FULL_SCREEN_USER_SWITCHER, true) },
-                mocks =
-                    TestMocksModule(
-                        notificationStackSizeCalculator = notificationStackSizeCalculator,
-                        notificationStackScrollLayoutController =
-                            notificationStackScrollLayoutController,
-                    )
+                mocks = TestMocksModule(),
             )
 
     @Test
@@ -336,17 +319,9 @@
     @Test
     fun maxNotificationsOnLockscreen() =
         testComponent.runTest {
-            whenever(
-                    notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                        any(),
-                        any(),
-                        any(),
-                        any()
-                    )
-                )
-                .thenReturn(10)
-
-            val maxNotifications by collectLastValue(underTest.maxNotifications)
+            var notificationCount = 10
+            val maxNotifications by
+                collectLastValue(underTest.getMaxNotifications { notificationCount })
 
             showLockscreen()
 
@@ -356,21 +331,52 @@
                 SharedNotificationContainerPosition(top = 1f, bottom = 2f)
 
             assertThat(maxNotifications).isEqualTo(10)
+
+            // Also updates when directly requested (as it would from NotificationStackScrollLayout)
+            notificationCount = 25
+            sharedNotificationContainerInteractor.notificationStackChanged()
+            assertThat(maxNotifications).isEqualTo(25)
+        }
+
+    @Test
+    fun maxNotificationsOnLockscreen_DoesNotUpdateWhenUserInteracting() =
+        testComponent.runTest {
+            var notificationCount = 10
+            val maxNotifications by
+                collectLastValue(underTest.getMaxNotifications { notificationCount })
+
+            showLockscreen()
+
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            configurationRepository.onAnyConfigurationChange()
+            keyguardInteractor.sharedNotificationContainerPosition.value =
+                SharedNotificationContainerPosition(top = 1f, bottom = 2f)
+
+            assertThat(maxNotifications).isEqualTo(10)
+
+            // Shade expanding... still 10
+            shadeRepository.setLockscreenShadeExpansion(0.5f)
+            assertThat(maxNotifications).isEqualTo(10)
+
+            notificationCount = 25
+
+            // When shade is expanding by user interaction
+            shadeRepository.setLegacyLockscreenShadeTracking(true)
+
+            // Should still be 10, since the user is interacting
+            assertThat(maxNotifications).isEqualTo(10)
+
+            shadeRepository.setLegacyLockscreenShadeTracking(false)
+            shadeRepository.setLockscreenShadeExpansion(0f)
+
+            // Stopped tracking, show 25
+            assertThat(maxNotifications).isEqualTo(25)
         }
 
     @Test
     fun maxNotificationsOnShade() =
         testComponent.runTest {
-            whenever(
-                    notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                        any(),
-                        any(),
-                        any(),
-                        any()
-                    )
-                )
-                .thenReturn(10)
-            val maxNotifications by collectLastValue(underTest.maxNotifications)
+            val maxNotifications by collectLastValue(underTest.getMaxNotifications { 10 })
 
             // Show lockscreen with shade expanded
             showLockscreenWithShadeExpanded()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index ddfe79a..6e9363b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -20,16 +20,16 @@
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.authentication.shared.model.AuthenticationResultModel
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -37,12 +37,13 @@
 import kotlinx.coroutines.test.currentTime
 
 class FakeAuthenticationRepository(
-    private val deviceEntryRepository: FakeDeviceEntryRepository,
     private val currentTime: () -> Long,
 ) : AuthenticationRepository {
 
-    private val _isAutoConfirmEnabled = MutableStateFlow(false)
-    override val isAutoConfirmEnabled: StateFlow<Boolean> = _isAutoConfirmEnabled.asStateFlow()
+    private val _isAutoConfirmFeatureEnabled = MutableStateFlow(false)
+    override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
+        _isAutoConfirmFeatureEnabled.asStateFlow()
+    override val authenticationChallengeResult = MutableSharedFlow<Boolean>()
 
     override val hintedPinLength: Int = HINTING_PIN_LENGTH
 
@@ -79,7 +80,7 @@
 
     override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
         failedAttemptCount = if (isSuccessful) 0 else failedAttemptCount + 1
-        deviceEntryRepository.setUnlocked(isSuccessful)
+        authenticationChallengeResult.emit(isSuccessful)
     }
 
     override suspend fun getPinLength(): Int {
@@ -98,8 +99,8 @@
         _throttling.value = throttlingModel
     }
 
-    fun setAutoConfirmEnabled(isEnabled: Boolean) {
-        _isAutoConfirmEnabled.value = isEnabled
+    fun setAutoConfirmFeatureEnabled(isEnabled: Boolean) {
+        _isAutoConfirmFeatureEnabled.value = isEnabled
     }
 
     override suspend fun setThrottleDuration(durationMs: Int) {
@@ -215,9 +216,8 @@
     @Provides
     @SysUISingleton
     fun provideFake(
-        deviceEntryRepository: FakeDeviceEntryRepository,
         scope: TestScope,
-    ) = FakeAuthenticationRepository(deviceEntryRepository, currentTime = { scope.currentTime })
+    ) = FakeAuthenticationRepository(currentTime = { scope.currentTime })
 
     @Module
     interface Bindings {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index 8a2ff50..c6f12e2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -1,22 +1,14 @@
 package com.android.systemui.communal.data.repository
 
-import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Fake implementation of [CommunalWidgetRepository] */
 class FakeCommunalWidgetRepository : CommunalWidgetRepository {
-    private val _stopwatchAppWidgetInfo = MutableStateFlow<CommunalAppWidgetInfo?>(null)
-    override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = _stopwatchAppWidgetInfo
-
     private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList())
     override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets
 
-    fun setStopwatchAppWidgetInfo(appWidgetInfo: CommunalAppWidgetInfo) {
-        _stopwatchAppWidgetInfo.value = appWidgetInfo
-    }
-
     fun setCommunalWidgets(inventory: List<CommunalWidgetContentModel>) {
         _communalWidgets.value = inventory
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
index f029348..ba70d46 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
@@ -27,7 +27,7 @@
 @SysUISingleton
 class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository {
 
-    private var isInsecureLockscreenEnabled = true
+    private var isLockscreenEnabled = true
 
     private val _isBypassEnabled = MutableStateFlow(false)
     override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled
@@ -35,16 +35,20 @@
     private val _isUnlocked = MutableStateFlow(false)
     override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
 
-    override suspend fun isInsecureLockscreenEnabled(): Boolean {
-        return isInsecureLockscreenEnabled
+    override suspend fun isLockscreenEnabled(): Boolean {
+        return isLockscreenEnabled
+    }
+
+    override fun reportSuccessfulAuthentication() {
+        _isUnlocked.value = true
     }
 
     fun setUnlocked(isUnlocked: Boolean) {
         _isUnlocked.value = isUnlocked
     }
 
-    fun setInsecureLockscreenEnabled(isLockscreenEnabled: Boolean) {
-        this.isInsecureLockscreenEnabled = isLockscreenEnabled
+    fun setLockscreenEnabled(isLockscreenEnabled: Boolean) {
+        this.isLockscreenEnabled = isLockscreenEnabled
     }
 
     fun setBypassEnabled(isBypassEnabled: Boolean) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 36ec18f..72cc08f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -26,11 +26,9 @@
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.AuthenticationRepository
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
@@ -110,7 +108,6 @@
     val deviceEntryRepository: FakeDeviceEntryRepository by lazy { FakeDeviceEntryRepository() }
     val authenticationRepository: FakeAuthenticationRepository by lazy {
         FakeAuthenticationRepository(
-            deviceEntryRepository = deviceEntryRepository,
             currentTime = { testScope.currentTime },
         )
     }
@@ -181,6 +178,7 @@
             sceneInteractor = sceneInteractor,
             deviceEntryFaceAuthRepository = faceAuthRepository,
             trustRepository = trustRepository,
+            flags = FakeSceneContainerFlags(enabled = true)
         )
     }
 
@@ -192,7 +190,6 @@
             repository = repository,
             backgroundDispatcher = testDispatcher,
             userRepository = userRepository,
-            deviceEntryRepository = deviceEntryRepository,
             clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } }
         )
     }
@@ -221,17 +218,13 @@
     }
 
     fun bouncerInteractor(
-        deviceEntryInteractor: DeviceEntryInteractor,
         authenticationInteractor: AuthenticationInteractor,
-        sceneInteractor: SceneInteractor,
     ): BouncerInteractor {
         return BouncerInteractor(
             applicationScope = applicationScope(),
             applicationContext = context,
             repository = BouncerRepository(featureFlags),
-            deviceEntryInteractor = deviceEntryInteractor,
             authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
             flags = sceneContainerFlags,
             falsingInteractor = falsingInteractor(),
         )
@@ -345,19 +338,4 @@
             dozeLogger = dozeLogger,
         )
     }
-
-    companion object {
-        fun DomainLayerAuthenticationMethodModel.toDataLayer(): DataLayerAuthenticationMethodModel {
-            return when (this) {
-                DomainLayerAuthenticationMethodModel.None -> DataLayerAuthenticationMethodModel.None
-                DomainLayerAuthenticationMethodModel.Swipe ->
-                    DataLayerAuthenticationMethodModel.None
-                DomainLayerAuthenticationMethodModel.Pin -> DataLayerAuthenticationMethodModel.Pin
-                DomainLayerAuthenticationMethodModel.Password ->
-                    DataLayerAuthenticationMethodModel.Password
-                DomainLayerAuthenticationMethodModel.Pattern ->
-                    DataLayerAuthenticationMethodModel.Pattern
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 800593f..02318ab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -59,6 +59,8 @@
     private val _legacyIsQsExpanded = MutableStateFlow(false)
     @Deprecated("Use ShadeInteractor instead") override val legacyIsQsExpanded = _legacyIsQsExpanded
 
+    override val legacyLockscreenShadeTracking = MutableStateFlow(false)
+
     @Deprecated("Use ShadeInteractor instead")
     override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) {
         _legacyIsQsExpanded.value = legacyIsQsExpanded
@@ -81,6 +83,11 @@
         _legacyShadeTracking.value = tracking
     }
 
+    @Deprecated("Should only be called by NPVC and tests")
+    override fun setLegacyLockscreenShadeTracking(tracking: Boolean) {
+        legacyLockscreenShadeTracking.value = tracking
+    }
+
     fun setShadeModel(model: ShadeModel) {
         _shadeModel.value = model
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt
new file mode 100644
index 0000000..0c2b115
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt
@@ -0,0 +1,31 @@
+package com.android.systemui.statusbar.policy
+
+class FakeDeviceProvisionedController : DeviceProvisionedController {
+    @JvmField var deviceProvisioned = true
+
+    override fun addCallback(listener: DeviceProvisionedController.DeviceProvisionedListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeCallback(listener: DeviceProvisionedController.DeviceProvisionedListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun isDeviceProvisioned() = deviceProvisioned
+
+    override fun getCurrentUser(): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun isUserSetup(user: Int): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun isCurrentUserSetup(): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun isFrpActive(): Boolean {
+        TODO("Not yet implemented")
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
index bdf1aff..d452810 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
@@ -24,6 +24,9 @@
 
     private final BaseLeakChecker<Callback> mCallbackController;
 
+    private boolean mIsShowing = false;
+    private boolean mIsOccluded = false;
+
     public FakeKeyguardStateController(LeakCheck test) {
         mCallbackController = new BaseLeakChecker<Callback>(test, "keyguard");
     }
@@ -45,7 +48,11 @@
 
     @Override
     public boolean isShowing() {
-        return false;
+        return mIsShowing;
+    }
+
+    public void setShowing(boolean showing) {
+        mIsShowing = showing;
     }
 
     @Override
@@ -60,7 +67,11 @@
 
     @Override
     public boolean isOccluded() {
-        return false;
+        return mIsOccluded;
+    }
+
+    public void setOccluded(boolean occluded) {
+        mIsOccluded = occluded;
     }
 
     @Override
diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS
index 83143a4..5295ec8 100644
--- a/services/companion/java/com/android/server/companion/virtual/OWNERS
+++ b/services/companion/java/com/android/server/companion/virtual/OWNERS
@@ -1,3 +1,5 @@
+# Bug component: 1171888
+
 set noparent
 
 ogunwale@google.com
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 15fc2dc..f6835fe 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3651,7 +3651,26 @@
             Watchdog.getInstance().pauseWatchingMonitorsFor(
                 SLOW_OPERATION_WATCHDOG_TIMEOUT_MS, "#close might be slow");
             if (mMounted) {
-                mVold.unmountAppFuse(uid, mountId);
+                BackgroundThread.getHandler().post(() -> {
+                    try {
+                        // We need to run the unmount on a separate thread to
+                        // prevent a possible deadlock, where:
+                        // 1. AppFuseThread (this thread) tries to call into vold
+                        // 2. the vold lock is held by another thread, which called:
+                        //    mVold.openAppFuseFile()
+                        //    as part of that call, vold calls open() on the
+                        //    underlying file, which is a call that needs to be
+                        //    handled by the AppFuseThread, which is stuck waiting
+                        //    for the vold lock (see 1.)
+                        // It is safe to do the unmount asynchronously, because the mount
+                        // path we use is never reused during the current boot cycle;
+                        // see mNextAppFuseName. Also,we have anyway stopped serving
+                        // requests at this point.
+                        mVold.unmountAppFuse(uid, mountId);
+                    } catch (RemoteException e) {
+                        throw e.rethrowAsRuntimeException();
+                    }
+                });
                 mMounted = false;
             }
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b99a98f..f92af67 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -599,6 +599,9 @@
     private static final String INTENT_REMOTE_BUGREPORT_FINISHED =
             "com.android.internal.intent.action.REMOTE_BUGREPORT_FINISHED";
 
+    public static final String DATA_FILE_PATH_HEADER = "Data File: ";
+    public static final String DATA_FILE_PATH_FOOTER = "End Data File\n";
+
     // If set, we will push process association information in to procstats.
     static final boolean TRACK_PROCSTATS_ASSOCIATIONS = true;
 
@@ -9595,17 +9598,33 @@
                         : Settings.Global.getInt(mContext.getContentResolver(), logcatSetting, 0);
                 int dropboxMaxSize = Settings.Global.getInt(
                         mContext.getContentResolver(), maxBytesSetting, DROPBOX_DEFAULT_MAX_SIZE);
-                int maxDataFileSize = dropboxMaxSize - sb.length()
-                        - lines * RESERVED_BYTES_PER_LOGCAT_LINE;
 
-                if (dataFile != null && maxDataFileSize > 0) {
-                    try {
-                        sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
-                                    "\n\n[[TRUNCATED]]"));
-                    } catch (IOException e) {
-                        Slog.e(TAG, "Error reading " + dataFile, e);
+                if (dataFile != null) {
+                    // Attach the stack traces file to the report so collectors can load them
+                    // by file if they have access.
+                    sb.append(DATA_FILE_PATH_HEADER)
+                            .append(dataFile.getAbsolutePath()).append('\n');
+
+                    int maxDataFileSize = dropboxMaxSize
+                            - sb.length()
+                            - lines * RESERVED_BYTES_PER_LOGCAT_LINE
+                            - DATA_FILE_PATH_FOOTER.length();
+
+                    if (maxDataFileSize > 0) {
+                        // Inline dataFile contents if there is room.
+                        try {
+                            sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
+                                    "\n\n[[TRUNCATED]]\n"));
+                        } catch (IOException e) {
+                            Slog.e(TAG, "Error reading " + dataFile, e);
+                        }
                     }
+
+                    // Always append the footer, even there wasn't enough space to inline the
+                    // dataFile contents.
+                    sb.append(DATA_FILE_PATH_FOOTER);
                 }
+
                 if (crashInfo != null && crashInfo.stackTrace != null) {
                     sb.append(crashInfo.stackTrace);
                 }
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index e41b6ae..3ce92bc 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -265,13 +265,6 @@
     @Nullable
     public BroadcastRecord enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record,
             int recordIndex, @NonNull BroadcastConsumer deferredStatesApplyConsumer) {
-        // When updateDeferredStates() has already applied a deferred state to
-        // all pending items, apply to this new broadcast too
-        if (mLastDeferredStates && record.deferUntilActive
-                && (record.getDeliveryState(recordIndex) == BroadcastRecord.DELIVERY_PENDING)) {
-            deferredStatesApplyConsumer.accept(record, recordIndex);
-        }
-
         // Ignore FLAG_RECEIVER_REPLACE_PENDING if the sender specified the policy using the
         // BroadcastOptions delivery group APIs.
         if (record.isReplacePending()
@@ -294,6 +287,13 @@
         // with implicit responsiveness expectations.
         getQueueForBroadcast(record).addLast(newBroadcastArgs);
         onBroadcastEnqueued(record, recordIndex);
+
+        // When updateDeferredStates() has already applied a deferred state to
+        // all pending items, apply to this new broadcast too
+        if (mLastDeferredStates && shouldBeDeferred()
+                && (record.getDeliveryState(recordIndex) == BroadcastRecord.DELIVERY_PENDING)) {
+            deferredStatesApplyConsumer.accept(record, recordIndex);
+        }
         return null;
     }
 
@@ -1235,32 +1235,45 @@
     }
 
     /**
-     * Update {@link BroadcastRecord.DELIVERY_DEFERRED} states of all our
+     * Update {@link BroadcastRecord#DELIVERY_DEFERRED} states of all our
      * pending broadcasts, when needed.
      */
     void updateDeferredStates(@NonNull BroadcastConsumer applyConsumer,
             @NonNull BroadcastConsumer clearConsumer) {
         // When all we have pending is deferred broadcasts, and we're cached,
         // then we want everything to be marked deferred
-        final boolean wantDeferredStates = (mCountDeferred > 0)
-                && (mCountDeferred == mCountEnqueued) && mProcessFreezable;
+        final boolean wantDeferredStates = shouldBeDeferred();
 
         if (mLastDeferredStates != wantDeferredStates) {
             mLastDeferredStates = wantDeferredStates;
             if (wantDeferredStates) {
                 forEachMatchingBroadcast((r, i) -> {
-                    return r.deferUntilActive
-                            && (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_PENDING);
+                    return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_PENDING);
                 }, applyConsumer, false);
             } else {
                 forEachMatchingBroadcast((r, i) -> {
-                    return r.deferUntilActive
-                            && (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
+                    return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
                 }, clearConsumer, false);
             }
         }
     }
 
+    void clearDeferredStates(@NonNull BroadcastConsumer clearConsumer) {
+        if (mLastDeferredStates) {
+            mLastDeferredStates = false;
+            forEachMatchingBroadcast((r, i) -> {
+                return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
+            }, clearConsumer, false);
+        }
+    }
+
+    @VisibleForTesting
+    boolean shouldBeDeferred() {
+        if (mRunnableAtInvalidated) updateRunnableAt();
+        return mRunnableAtReason == REASON_CACHED
+                || mRunnableAtReason == REASON_CACHED_INFINITE_DEFER;
+    }
+
     /**
      * Check overall health, confirming things are in a reasonable state and
      * that we're not wedged.
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index b481697..5b54561 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -479,6 +479,10 @@
                 break;
             }
 
+            // Clear the deferred state of broadcasts in this queue as we are just about to
+            // deliver broadcasts to this process.
+            queue.clearDeferredStates(mBroadcastConsumerDeferClear);
+
             // We might not have heard about a newly running process yet, so
             // consider refreshing if we think we're cold
             updateWarmProcess(queue);
@@ -1567,12 +1571,14 @@
         r.resultExtras = null;
     };
 
-    private final BroadcastConsumer mBroadcastConsumerDeferApply = (r, i) -> {
+    @VisibleForTesting
+    final BroadcastConsumer mBroadcastConsumerDeferApply = (r, i) -> {
         setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_DEFERRED,
                 "mBroadcastConsumerDeferApply");
     };
 
-    private final BroadcastConsumer mBroadcastConsumerDeferClear = (r, i) -> {
+    @VisibleForTesting
+    final BroadcastConsumer mBroadcastConsumerDeferClear = (r, i) -> {
         setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_PENDING,
                 "mBroadcastConsumerDeferClear");
     };
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index a0a7b2b..d0ab287 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -1472,10 +1472,13 @@
             }
             return;
         }
+        boolean processFreezableChangeReported = false;
         if (opt.isPendingFreeze()) {
             // Remove pending DO_FREEZE message
             mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app);
             opt.setPendingFreeze(false);
+            reportProcessFreezableChangedLocked(app);
+            processFreezableChangeReported = true;
             if (DEBUG_FREEZER) {
                 Slog.d(TAG_AM, "Cancel freezing " + pid + " " + app.processName);
             }
@@ -1524,7 +1527,9 @@
         if (processKilled) {
             return;
         }
-        reportProcessFreezableChangedLocked(app);
+        if (!processFreezableChangeReported) {
+            reportProcessFreezableChangedLocked(app);
+        }
 
         long freezeTime = opt.getFreezeUnfreezeTime();
 
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 052b0c2..e5f7637 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2391,7 +2391,8 @@
         long token = Binder.clearCallingIdentity();
         try {
             // Permissions are managed by UIDs, but unfortunately a package name is required in API.
-            String packageName = ArrayUtils.firstOrNull(packageManager.getPackagesForUid(uid));
+            String packageName = ArrayUtils.firstOrNull(ArrayUtils.defeatNullable(
+                    packageManager.getPackagesForUid(uid)));
             if (packageName == null) {
                 return false;
             }
diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java
index 51cb950..5c8dd0d 100644
--- a/services/core/java/com/android/server/audio/AdiDeviceState.java
+++ b/services/core/java/com/android/server/audio/AdiDeviceState.java
@@ -25,6 +25,7 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.media.Utils;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -167,8 +168,9 @@
     public String toString() {
         return "type: " + mDeviceType
                 + " internal type: 0x" + Integer.toHexString(mInternalDeviceType)
-                + " addr: " + mDeviceAddress + " bt audio type: "
-                + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory)
+                + " addr: " + Utils.anonymizeBluetoothAddress(mInternalDeviceType, mDeviceAddress)
+                + " bt audio type: "
+                        + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory)
                 + " enabled: " + mSAEnabled + " HT: " + mHasHeadTracker
                 + " HTenabled: " + mHeadTrackerEnabled;
     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 7ba0827..e9b102b 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -48,6 +48,7 @@
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.MediaMetrics;
 import android.media.MediaRecorder.AudioSource;
+import android.media.Utils;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.SafeCloseable;
@@ -477,7 +478,7 @@
             return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
                     + " (" + AudioSystem.getDeviceName(mDeviceType)
                     + ") name:" + mDeviceName
-                    + " addr:" + mDeviceAddress
+                    + " addr:" + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceAddress)
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
                     + " peer addr:" + mPeerDeviceAddress
                     + " group:" + mGroupId
@@ -532,7 +533,7 @@
         mApmConnectedDevices.forEach((keyType, valueAddress) -> {
             pw.println("  " + prefix + " type:0x" + Integer.toHexString(keyType)
                     + " (" + AudioSystem.getDeviceName(keyType)
-                    + ") addr:" + valueAddress); });
+                    + ") addr:" + Utils.anonymizeBluetoothAddress(keyType, valueAddress)); });
         pw.println("\n" + prefix + "Preferred devices for capture preset:");
         mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
             pw.println("  " + prefix + "capturePreset:" + capturePreset
@@ -1789,7 +1790,8 @@
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "A2DP device addr=" + address + " now available").printLog(TAG));
+                    "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
+                            + " now available").printLog(TAG));
         }
 
         // Reset A2DP suspend state each time a new sink is connected
@@ -2027,7 +2029,8 @@
                 .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) {
             // removing A2DP device not currently used by AudioPolicy, log but don't act on it
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                    "A2DP device " + address + " made unavailable, was not used")).printLog(TAG));
+                    "A2DP device " + Utils.anonymizeBluetoothAddress(address)
+                            + " made unavailable, was not used")).printLog(TAG));
             mmi.set(MediaMetrics.Property.EARLY_RETURN,
                     "A2DP device made unavailable, was not used")
                     .record();
@@ -2043,13 +2046,15 @@
 
         if (res != AudioSystem.AUDIO_STATUS_OK) {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "APM failed to make unavailable A2DP device addr=" + address
+                    "APM failed to make unavailable A2DP device addr="
+                            + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printLog(TAG));
             // TODO:  failed to disconnect, stop here
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                    "A2DP device addr=" + address + " made unavailable")).printLog(TAG));
+                    "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
+                            + " made unavailable")).printLog(TAG));
         }
         mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
 
@@ -2238,7 +2243,8 @@
                 // TODO: return;
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "LE Audio device addr=" + address + " now available").printLog(TAG));
+                        "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
+                                + " now available").printLog(TAG));
             }
             // Reset LEA suspend state each time a new sink is connected
             mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
@@ -2282,7 +2288,8 @@
                 // TODO: return;
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "LE Audio device addr=" + address + " made unavailable").printLog(TAG));
+                        "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
+                                + " made unavailable").printLog(TAG));
             }
             mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index b2ee610..1e38c0f 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -136,6 +136,7 @@
 import android.media.MediaRecorder.AudioSource;
 import android.media.PlayerBase;
 import android.media.Spatializer;
+import android.media.Utils;
 import android.media.VolumeInfo;
 import android.media.VolumePolicy;
 import android.media.audiofx.AudioEffect;
@@ -7470,7 +7471,7 @@
 
         sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
                 + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:"
-                + device.getAddress() + " behavior:"
+                + Utils.anonymizeBluetoothAddress(device.getAddress()) + " behavior:"
                 + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior)
                 + " pack:" + pkgName).printLog(TAG));
         if (pkgName == null) {
@@ -9641,7 +9642,7 @@
     private void avrcpSupportsAbsoluteVolume(String address, boolean support) {
         // address is not used for now, but may be used when multiple a2dp devices are supported
         sVolumeLogger.enqueue(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
-                + address + " support=" + support).printLog(TAG));
+                + Utils.anonymizeBluetoothAddress(address) + " support=" + support).printLog(TAG));
         mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
         setAvrcpAbsoluteVolumeSupported(support);
     }
@@ -10539,11 +10540,11 @@
     AudioDeviceAttributes retrieveBluetoothAddressUncheked(@NonNull AudioDeviceAttributes ada) {
         Objects.requireNonNull(ada);
         if (AudioSystem.isBluetoothDevice(ada.getInternalType())) {
-            String anonymizedAddress = anonymizeBluetoothAddress(ada.getAddress());
+            String anonymizedAddress = Utils.anonymizeBluetoothAddress(ada.getAddress());
             for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) {
                 if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType())
                         && (ada.getInternalType() == ads.getInternalDeviceType())
-                        && anonymizedAddress.equals(anonymizeBluetoothAddress(
+                        && anonymizedAddress.equals(Utils.anonymizeBluetoothAddress(
                                 ads.getDeviceAddress())))) {
                     continue;
                 }
@@ -10554,19 +10555,6 @@
         return ada;
     }
 
-    /**
-     * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
-     * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
-     * @param address Mac address to be anonymized
-     * @return anonymized mac address
-     */
-    static String anonymizeBluetoothAddress(String address) {
-        if (address == null || address.length() != "AA:BB:CC:DD:EE:FF".length()) {
-            return null;
-        }
-        return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
-    }
-
     private List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesList(
                 List<AudioDeviceAttributes> devices) {
         if (isBluetoothPrividged()) {
@@ -10590,7 +10578,7 @@
             return ada;
         }
         AudioDeviceAttributes res = new AudioDeviceAttributes(ada);
-        res.setAddress(anonymizeBluetoothAddress(ada.getAddress()));
+        res.setAddress(Utils.anonymizeBluetoothAddress(ada.getAddress()));
         return res;
     }
 
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 53fbe8f..a12243b 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -22,7 +22,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.RouteInfo.RTN_THROW;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN;
 import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
@@ -45,12 +44,10 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -113,7 +110,6 @@
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.FileUtils;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.INetworkManagementService;
@@ -152,7 +148,6 @@
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.LinkPropertiesUtils;
 import com.android.net.module.util.NetdUtils;
@@ -202,7 +197,6 @@
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * @hide
@@ -1063,8 +1057,6 @@
         // Store mPackage since it might be reset or might be replaced with the other VPN app.
         final String oldPackage = mPackage;
         final boolean isPackageChanged = !Objects.equals(packageName, oldPackage);
-        // TODO: Remove "SdkLevel.isAtLeastT()" check once VpnManagerService is decoupled from
-        //  ConnectivityServiceTest.
         // Only notify VPN apps that were already always-on, and only if the always-on provider
         // changed, or the lockdown mode changed.
         final boolean shouldNotifyOldPkg = isVpnApp(oldPackage) && mAlwaysOn
@@ -1078,12 +1070,6 @@
 
         saveAlwaysOnPackage();
 
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
-        if (!SdkLevel.isAtLeastT()) {
-            return true;
-        }
-
         if (shouldNotifyOldPkg) {
             // If both of shouldNotifyOldPkg & isPackageChanged are true, that means the
             // always-on of old package is disabled or the old package is replaced with the new
@@ -1984,9 +1970,7 @@
         for (String app : packageNames) {
             int uid = getAppUid(mContext, app, userId);
             if (uid != -1) uids.add(uid);
-            // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-            // ConnectivityServiceTest.
-            if (Process.isApplicationUid(uid) && SdkLevel.isAtLeastT()) {
+            if (Process.isApplicationUid(uid)) {
                 uids.add(Process.toSdkSandboxUid(uid));
             }
         }
@@ -2297,15 +2281,6 @@
 
     private INetworkManagementEventObserver mObserver = new BaseNetworkObserver() {
         @Override
-        public void interfaceStatusChanged(String interfaze, boolean up) {
-            synchronized (Vpn.this) {
-                if (!up && mVpnRunner != null && mVpnRunner instanceof LegacyVpnRunner) {
-                    ((LegacyVpnRunner) mVpnRunner).exitIfOuterInterfaceIs(interfaze);
-                }
-            }
-        }
-
-        @Override
         public void interfaceRemoved(String interfaze) {
             synchronized (Vpn.this) {
                 if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
@@ -2556,17 +2531,6 @@
     private native boolean jniAddAddress(String interfaze, String address, int prefixLen);
     private native boolean jniDelAddress(String interfaze, String address, int prefixLen);
 
-    private static RouteInfo findIPv4DefaultRoute(LinkProperties prop) {
-        for (RouteInfo route : prop.getAllRoutes()) {
-            // Currently legacy VPN only works on IPv4.
-            if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) {
-                return route;
-            }
-        }
-
-        throw new IllegalStateException("Unable to find IPv4 default gateway");
-    }
-
     private void enforceNotRestrictedUser() {
         final long token = Binder.clearCallingIdentity();
         try {
@@ -2665,10 +2629,6 @@
             throw new SecurityException("Restricted users cannot establish VPNs");
         }
 
-        final RouteInfo ipv4DefaultRoute = findIPv4DefaultRoute(egress);
-        final String gateway = ipv4DefaultRoute.getGateway().getHostAddress();
-        final String iface = ipv4DefaultRoute.getInterface();
-
         // Load certificates.
         String privateKey = "";
         String userCert = "";
@@ -2700,8 +2660,6 @@
             throw new IllegalStateException("Cannot load credentials");
         }
 
-        // Prepare arguments for racoon.
-        String[] racoon = null;
         switch (profile.type) {
             case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
                 // Secret key is still just the alias (not the actual private key). The private key
@@ -2731,109 +2689,9 @@
                 // profile.
                 startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN);
                 return;
-            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
-                racoon = new String[] {
-                    iface, profile.server, "udppsk", profile.ipsecIdentifier,
-                    profile.ipsecSecret, "1701",
-                };
-                break;
-            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "udprsa", makeKeystoreEngineGrantString(privateKey),
-                    userCert, caCert, serverCert, "1701",
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
-                racoon = new String[] {
-                    iface, profile.server, "xauthpsk", profile.ipsecIdentifier,
-                    profile.ipsecSecret, profile.username, profile.password, "", gateway,
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "xauthrsa", makeKeystoreEngineGrantString(privateKey),
-                    userCert, caCert, serverCert, profile.username, profile.password, "", gateway,
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "hybridrsa",
-                    caCert, serverCert, profile.username, profile.password, "", gateway,
-                };
-                break;
         }
 
-        // Prepare arguments for mtpd. MTU/MRU calculated conservatively. Only IPv4 supported
-        // because LegacyVpn.
-        // 1500 - 60 (Carrier-internal IPv6 + UDP + GTP) - 10 (PPP) - 16 (L2TP) - 8 (UDP)
-        //   - 77 (IPsec w/ SHA-2 512, 256b trunc-len, AES-CBC) - 8 (UDP encap) - 20 (IPv4)
-        //   - 28 (464xlat)
-        String[] mtpd = null;
-        switch (profile.type) {
-            case VpnProfile.TYPE_PPTP:
-                mtpd = new String[] {
-                    iface, "pptp", profile.server, "1723",
-                    "name", profile.username, "password", profile.password,
-                    "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270",
-                    (profile.mppe ? "+mppe" : "nomppe"),
-                };
-                if (profile.mppe) {
-                    // Disallow PAP authentication when MPPE is requested, as MPPE cannot work
-                    // with PAP anyway, and users may not expect PAP (plain text) to be used when
-                    // MPPE was requested.
-                    mtpd = Arrays.copyOf(mtpd, mtpd.length + 1);
-                    mtpd[mtpd.length - 1] = "-pap";
-                }
-                break;
-            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
-            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
-                mtpd = new String[] {
-                    iface, "l2tp", profile.server, "1701", profile.l2tpSecret,
-                    "name", profile.username, "password", profile.password,
-                    "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270",
-                };
-                break;
-        }
-
-        VpnConfig config = new VpnConfig();
-        config.legacy = true;
-        config.user = profile.key;
-        config.interfaze = iface;
-        config.session = profile.name;
-        config.isMetered = false;
-        config.proxyInfo = profile.proxy;
-        if (underlying != null) {
-            config.underlyingNetworks = new Network[] { underlying };
-        }
-
-        config.addLegacyRoutes(profile.routes);
-        if (!profile.dnsServers.isEmpty()) {
-            config.dnsServers = Arrays.asList(profile.dnsServers.split(" +"));
-        }
-        if (!profile.searchDomains.isEmpty()) {
-            config.searchDomains = Arrays.asList(profile.searchDomains.split(" +"));
-        }
-        startLegacyVpn(config, racoon, mtpd, profile);
-    }
-
-    private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd,
-            VpnProfile profile) {
-        stopVpnRunnerPrivileged();
-
-        // Prepare for the new request.
-        prepareInternal(VpnConfig.LEGACY_VPN);
-        updateState(DetailedState.CONNECTING, "startLegacyVpn");
-
-        // Start a new LegacyVpnRunner and we are done!
-        mVpnRunner = new LegacyVpnRunner(config, racoon, mtpd, profile);
-        startLegacyVpnRunner();
-    }
-
-    @VisibleForTesting
-    protected void startLegacyVpnRunner() {
-        mVpnRunner.start();
+        throw new UnsupportedOperationException("Legacy VPN is deprecated");
     }
 
     /**
@@ -2851,17 +2709,7 @@
             return;
         }
 
-        final boolean isLegacyVpn = mVpnRunner instanceof LegacyVpnRunner;
         mVpnRunner.exit();
-
-        // LegacyVpn uses daemons that must be shut down before new ones are brought up.
-        // The same limitation does not apply to Platform VPNs.
-        if (isLegacyVpn) {
-            synchronized (LegacyVpnRunner.TAG) {
-                // wait for old thread to completely finish before spinning up
-                // new instance, otherwise state updates can be out of order.
-            }
-        }
     }
 
     /**
@@ -4143,9 +3991,7 @@
                 // Ignore stale runner.
                 if (mVpnRunner != this) return;
 
-                // TODO(b/230548427): Remove SDK check once VPN related stuff are
-                //  decoupled from ConnectivityServiceTest.
-                if (SdkLevel.isAtLeastT() && category != null && isVpnApp(mPackage)) {
+                if (category != null && isVpnApp(mPackage)) {
                     sendEventToVpnManagerApp(category, errorClass, errorCode,
                             getPackage(), mSessionKey, makeVpnProfileStateLocked(),
                             mActiveNetwork,
@@ -4256,343 +4102,6 @@
         }
     }
 
-    /**
-     * Bringing up a VPN connection takes time, and that is all this thread
-     * does. Here we have plenty of time. The only thing we need to take
-     * care of is responding to interruptions as soon as possible. Otherwise
-     * requests will pile up. This could be done in a Handler as a state
-     * machine, but it is much easier to read in the current form.
-     */
-    private class LegacyVpnRunner extends VpnRunner {
-        private static final String TAG = "LegacyVpnRunner";
-
-        private final String[] mDaemons;
-        private final String[][] mArguments;
-        private final LocalSocket[] mSockets;
-        private final String mOuterInterface;
-        private final AtomicInteger mOuterConnection =
-                new AtomicInteger(ConnectivityManager.TYPE_NONE);
-        private final VpnProfile mProfile;
-
-        private long mBringupStartTime = -1;
-
-        /**
-         * Watch for the outer connection (passing in the constructor) going away.
-         */
-        private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (!mEnableTeardown) return;
-
-                if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
-                    if (intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE,
-                            ConnectivityManager.TYPE_NONE) == mOuterConnection.get()) {
-                        NetworkInfo info = (NetworkInfo)intent.getExtra(
-                                ConnectivityManager.EXTRA_NETWORK_INFO);
-                        if (info != null && !info.isConnectedOrConnecting()) {
-                            try {
-                                mObserver.interfaceStatusChanged(mOuterInterface, false);
-                            } catch (RemoteException e) {}
-                        }
-                    }
-                }
-            }
-        };
-
-        // GuardedBy("Vpn.this") (annotation can't be applied to constructor)
-        LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd, VpnProfile profile) {
-            super(TAG);
-            if (racoon == null && mtpd == null) {
-                throw new IllegalArgumentException(
-                        "Arguments to racoon and mtpd must not both be null");
-            }
-            mConfig = config;
-            mDaemons = new String[] {"racoon", "mtpd"};
-            // TODO: clear arguments from memory once launched
-            mArguments = new String[][] {racoon, mtpd};
-            mSockets = new LocalSocket[mDaemons.length];
-
-            // This is the interface which VPN is running on,
-            // mConfig.interfaze will change to point to OUR
-            // internal interface soon. TODO - add inner/outer to mconfig
-            // TODO - we have a race - if the outer iface goes away/disconnects before we hit this
-            // we will leave the VPN up.  We should check that it's still there/connected after
-            // registering
-            mOuterInterface = mConfig.interfaze;
-
-            mProfile = profile;
-
-            if (!TextUtils.isEmpty(mOuterInterface)) {
-                for (Network network : mConnectivityManager.getAllNetworks()) {
-                    final LinkProperties lp = mConnectivityManager.getLinkProperties(network);
-                    if (lp != null && lp.getAllInterfaceNames().contains(mOuterInterface)) {
-                        final NetworkInfo netInfo = mConnectivityManager.getNetworkInfo(network);
-                        if (netInfo != null) {
-                            mOuterConnection.set(netInfo.getType());
-                            break;
-                        }
-                    }
-                }
-            }
-
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
-            mContext.registerReceiver(mBroadcastReceiver, filter);
-        }
-
-        /**
-         * Checks if the parameter matches the underlying interface
-         *
-         * <p>If the underlying interface is torn down, the LegacyVpnRunner also should be. It has
-         * no ability to migrate between interfaces (or Networks).
-         */
-        public void exitIfOuterInterfaceIs(String interfaze) {
-            if (interfaze.equals(mOuterInterface)) {
-                Log.i(TAG, "Legacy VPN is going down with " + interfaze);
-                exitVpnRunner();
-            }
-        }
-
-        /** Tears down this LegacyVpn connection */
-        @Override
-        public void exitVpnRunner() {
-            // We assume that everything is reset after stopping the daemons.
-            interrupt();
-
-            // Always disconnect. This may be called again in cleanupVpnStateLocked() if
-            // exitVpnRunner() was called from exit(), but it will be a no-op.
-            agentDisconnect();
-            try {
-                mContext.unregisterReceiver(mBroadcastReceiver);
-            } catch (IllegalArgumentException e) {}
-        }
-
-        @Override
-        public void run() {
-            // Wait for the previous thread since it has been interrupted.
-            Log.v(TAG, "Waiting");
-            synchronized (TAG) {
-                Log.v(TAG, "Executing");
-                try {
-                    bringup();
-                    waitForDaemonsToStop();
-                    interrupted(); // Clear interrupt flag if execute called exit.
-                } catch (InterruptedException e) {
-                } finally {
-                    for (LocalSocket socket : mSockets) {
-                        IoUtils.closeQuietly(socket);
-                    }
-                    // This sleep is necessary for racoon to successfully complete sending delete
-                    // message to server.
-                    try {
-                        Thread.sleep(50);
-                    } catch (InterruptedException e) {
-                    }
-                    for (String daemon : mDaemons) {
-                        mDeps.stopService(daemon);
-                    }
-                }
-                agentDisconnect();
-            }
-        }
-
-        private void checkInterruptAndDelay(boolean sleepLonger) throws InterruptedException {
-            long now = SystemClock.elapsedRealtime();
-            if (now - mBringupStartTime <= 60000) {
-                Thread.sleep(sleepLonger ? 200 : 1);
-            } else {
-                updateState(DetailedState.FAILED, "checkpoint");
-                throw new IllegalStateException("VPN bringup took too long");
-            }
-        }
-
-        private void checkAndFixupArguments(@NonNull final InetAddress endpointAddress) {
-            final String endpointAddressString = endpointAddress.getHostAddress();
-            // Perform some safety checks before inserting the address in place.
-            // Position 0 in mDaemons and mArguments must be racoon, and position 1 must be mtpd.
-            if (!"racoon".equals(mDaemons[0]) || !"mtpd".equals(mDaemons[1])) {
-                throw new IllegalStateException("Unexpected daemons order");
-            }
-
-            // Respectively, the positions at which racoon and mtpd take the server address
-            // argument are 1 and 2. Not all types of VPN require both daemons however, and
-            // in that case the corresponding argument array is null.
-            if (mArguments[0] != null) {
-                if (!mProfile.server.equals(mArguments[0][1])) {
-                    throw new IllegalStateException("Invalid server argument for racoon");
-                }
-                mArguments[0][1] = endpointAddressString;
-            }
-
-            if (mArguments[1] != null) {
-                if (!mProfile.server.equals(mArguments[1][2])) {
-                    throw new IllegalStateException("Invalid server argument for mtpd");
-                }
-                mArguments[1][2] = endpointAddressString;
-            }
-        }
-
-        private void bringup() {
-            // Catch all exceptions so we can clean up a few things.
-            try {
-                // resolve never returns null. If it does because of some bug, it will be
-                // caught by the catch() block below and cleanup gracefully.
-                final InetAddress endpointAddress = mDeps.resolve(mProfile.server);
-
-                // Big hack : dynamically replace the address of the server in the arguments
-                // with the resolved address.
-                checkAndFixupArguments(endpointAddress);
-
-                // Initialize the timer.
-                mBringupStartTime = SystemClock.elapsedRealtime();
-
-                // Wait for the daemons to stop.
-                for (String daemon : mDaemons) {
-                    while (!mDeps.isServiceStopped(daemon)) {
-                        checkInterruptAndDelay(true);
-                    }
-                }
-
-                // Clear the previous state.
-                final File state = mDeps.getStateFile();
-                state.delete();
-                if (state.exists()) {
-                    throw new IllegalStateException("Cannot delete the state");
-                }
-                new File("/data/misc/vpn/abort").delete();
-
-                updateState(DetailedState.CONNECTING, "execute");
-
-                // Start the daemon with arguments.
-                for (int i = 0; i < mDaemons.length; ++i) {
-                    String[] arguments = mArguments[i];
-                    if (arguments == null) {
-                        continue;
-                    }
-
-                    // Start the daemon.
-                    String daemon = mDaemons[i];
-                    mDeps.startService(daemon);
-
-                    // Wait for the daemon to start.
-                    while (!mDeps.isServiceRunning(daemon)) {
-                        checkInterruptAndDelay(true);
-                    }
-
-                    // Create the control socket.
-                    mSockets[i] = new LocalSocket();
-
-                    // Wait for the socket to connect and send over the arguments.
-                    mDeps.sendArgumentsToDaemon(daemon, mSockets[i], arguments,
-                            this::checkInterruptAndDelay);
-                }
-
-                // Wait for the daemons to create the new state.
-                while (!state.exists()) {
-                    // Check if a running daemon is dead.
-                    for (int i = 0; i < mDaemons.length; ++i) {
-                        String daemon = mDaemons[i];
-                        if (mArguments[i] != null && !mDeps.isServiceRunning(daemon)) {
-                            throw new IllegalStateException(daemon + " is dead");
-                        }
-                    }
-                    checkInterruptAndDelay(true);
-                }
-
-                // Now we are connected. Read and parse the new state.
-                String[] parameters = FileUtils.readTextFile(state, 0, null).split("\n", -1);
-                if (parameters.length != 7) {
-                    throw new IllegalStateException("Cannot parse the state: '"
-                            + String.join("', '", parameters) + "'");
-                }
-
-                // Set the interface and the addresses in the config.
-                synchronized (Vpn.this) {
-                    mConfig.interfaze = parameters[0].trim();
-
-                    mConfig.addLegacyAddresses(parameters[1]);
-                    // Set the routes if they are not set in the config.
-                    if (mConfig.routes == null || mConfig.routes.isEmpty()) {
-                        mConfig.addLegacyRoutes(parameters[2]);
-                    }
-
-                    // Set the DNS servers if they are not set in the config.
-                    if (mConfig.dnsServers == null || mConfig.dnsServers.size() == 0) {
-                        String dnsServers = parameters[3].trim();
-                        if (!dnsServers.isEmpty()) {
-                            mConfig.dnsServers = Arrays.asList(dnsServers.split(" "));
-                        }
-                    }
-
-                    // Set the search domains if they are not set in the config.
-                    if (mConfig.searchDomains == null || mConfig.searchDomains.size() == 0) {
-                        String searchDomains = parameters[4].trim();
-                        if (!searchDomains.isEmpty()) {
-                            mConfig.searchDomains = Arrays.asList(searchDomains.split(" "));
-                        }
-                    }
-
-                    // Add a throw route for the VPN server endpoint, if one was specified.
-                    if (endpointAddress instanceof Inet4Address) {
-                        mConfig.routes.add(new RouteInfo(
-                                new IpPrefix(endpointAddress, 32), null /*gateway*/,
-                                null /*iface*/, RTN_THROW));
-                    } else if (endpointAddress instanceof Inet6Address) {
-                        mConfig.routes.add(new RouteInfo(
-                                new IpPrefix(endpointAddress, 128), null /*gateway*/,
-                                null /*iface*/, RTN_THROW));
-                    } else {
-                        Log.e(TAG, "Unknown IP address family for VPN endpoint: "
-                                + endpointAddress);
-                    }
-
-                    // Here is the last step and it must be done synchronously.
-                    // Set the start time
-                    mConfig.startTime = SystemClock.elapsedRealtime();
-
-                    // Check if the thread was interrupted while we were waiting on the lock.
-                    checkInterruptAndDelay(false);
-
-                    // Check if the interface is gone while we are waiting.
-                    if (!mDeps.isInterfacePresent(Vpn.this, mConfig.interfaze)) {
-                        throw new IllegalStateException(mConfig.interfaze + " is gone");
-                    }
-
-                    // Now INetworkManagementEventObserver is watching our back.
-                    mInterface = mConfig.interfaze;
-                    prepareStatusIntent();
-
-                    agentConnect();
-
-                    Log.i(TAG, "Connected!");
-                }
-            } catch (Exception e) {
-                Log.i(TAG, "Aborting", e);
-                updateState(DetailedState.FAILED, e.getMessage());
-                exitVpnRunner();
-            }
-        }
-
-        /**
-         * Check all daemons every two seconds. Return when one of them is stopped.
-         * The caller will move to the disconnected state when this function returns,
-         * which can happen if a daemon failed or if the VPN was torn down.
-         */
-        private void waitForDaemonsToStop() throws InterruptedException {
-            if (!mNetworkInfo.isConnected()) {
-                return;
-            }
-            while (true) {
-                Thread.sleep(2000);
-                for (int i = 0; i < mDaemons.length; i++) {
-                    if (mArguments[i] != null && mDeps.isServiceStopped(mDaemons[i])) {
-                        return;
-                    }
-                }
-            }
-        }
-    }
-
     private void verifyCallingUidAndPackage(String packageName) {
         mDeps.verifyCallingUidAndPackage(mContext, packageName, mUserId);
     }
@@ -4839,11 +4348,9 @@
         // Build intent first because the sessionKey will be reset after performing
         // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in
         // VpnRunner.exit() to prevent design being changed in the future.
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
         final int ownerUid = mOwnerUID;
         Intent intent = null;
-        if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
+        if (isVpnApp(mPackage)) {
             intent = buildVpnManagerEventIntent(
                     VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
                     -1 /* errorClass */, -1 /* errorCode*/, mPackage,
@@ -4884,12 +4391,8 @@
         // The underlying network, NetworkCapabilities and LinkProperties are not
         // necessary to send to VPN app since the purpose of this event is to notify
         // VPN app that VPN is deactivated by the user.
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
-        if (SdkLevel.isAtLeastT()) {
-            mEventChanges.log("[VMEvent] " + packageName + " stopped");
-            sendEventToVpnManagerApp(intent, packageName);
-        }
+        mEventChanges.log("[VMEvent] " + packageName + " stopped");
+        sendEventToVpnManagerApp(intent, packageName);
     }
 
     private boolean storeAppExclusionList(@NonNull String packageName,
diff --git a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
index 2d6b013..035a748 100644
--- a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
+++ b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
@@ -39,7 +39,6 @@
     private static final String TAG = AutofillSuggestionsController.class.getSimpleName();
 
     @NonNull private final InputMethodManagerService mService;
-    @NonNull private final InputMethodUtils.InputMethodSettings mSettings;
 
     private static final class CreateInlineSuggestionsRequest {
         @NonNull final InlineSuggestionsRequestInfo mRequestInfo;
@@ -76,7 +75,6 @@
 
     AutofillSuggestionsController(@NonNull InputMethodManagerService service) {
         mService = service;
-        mSettings = mService.mSettings;
     }
 
     @GuardedBy("ImfLock.class")
@@ -88,7 +86,7 @@
         final InputMethodInfo imi = mService.queryInputMethodForCurrentUserLocked(
                 mService.getSelectedMethodIdLocked());
         try {
-            if (userId == mSettings.getCurrentUserId()
+            if (userId == mService.getCurrentImeUserIdLocked()
                     && imi != null && isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) {
                 mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest(
                         requestInfo, callback, imi.getPackageName());
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index e76aa1a..c8c0482 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -63,7 +63,6 @@
 
     @NonNull private final InputMethodManagerService mService;
     @NonNull private final Context mContext;
-    @NonNull private final InputMethodUtils.InputMethodSettings mSettings;
     @NonNull private final PackageManagerInternal mPackageManagerInternal;
     @NonNull private final WindowManagerInternal mWindowManagerInternal;
 
@@ -113,7 +112,6 @@
             int imeConnectionBindFlags, CountDownLatch latchForTesting) {
         mService = service;
         mContext = mService.mContext;
-        mSettings = mService.mSettings;
         mPackageManagerInternal = mService.mPackageManagerInternal;
         mWindowManagerInternal = mService.mWindowManagerInternal;
         mImeConnectionBindFlags = imeConnectionBindFlags;
@@ -322,7 +320,7 @@
         private void updateCurrentMethodUid() {
             final String curMethodPackage = mCurIntent.getComponent().getPackageName();
             final int curMethodUid = mPackageManagerInternal.getPackageUid(
-                    curMethodPackage, 0 /* flags */, mSettings.getCurrentUserId());
+                    curMethodPackage, 0 /* flags */, mService.getCurrentImeUserIdLocked());
             if (curMethodUid < 0) {
                 Slog.e(TAG, "Failed to get UID for package=" + curMethodPackage);
                 mCurMethodUid = Process.INVALID_UID;
@@ -478,7 +476,7 @@
             return false;
         }
         return mContext.bindServiceAsUser(mCurIntent, conn, flags,
-                new UserHandle(mSettings.getCurrentUserId()));
+                new UserHandle(mService.getCurrentImeUserIdLocked()));
     }
 
     @GuardedBy("ImfLock.class")
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 1ae67dd..d656f20 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -20,6 +20,7 @@
 import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL;
 import static android.os.IServiceManager.DUMP_FLAG_PROTO;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
 import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE;
 import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED;
 import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION;
@@ -1727,6 +1728,12 @@
         registerDeviceListenerAndCheckStylusSupport();
     }
 
+    @GuardedBy("ImfLock.class")
+    @UserIdInt
+    int getCurrentImeUserIdLocked() {
+        return mSettings.getCurrentUserId();
+    }
+
     private final class InkWindowInitializer implements Runnable {
         public void run() {
             synchronized (ImfLock.class) {
@@ -6459,6 +6466,11 @@
                     if (!userHasDebugPriv(userId, shellCommand)) {
                         continue;
                     }
+                    // Skip on headless user
+                    if (USER_TYPE_SYSTEM_HEADLESS.equals(
+                            mUserManagerInternal.getUserInfo(userId).userType)) {
+                        continue;
+                    }
                     final String nextIme;
                     final List<InputMethodInfo> nextEnabledImes;
                     if (userId == mSettings.getCurrentUserId()) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index 86e417b..efa1e0d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -77,13 +77,15 @@
     void showInputMethodMenu(boolean showAuxSubtypes, int displayId) {
         if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
 
-        final boolean isScreenLocked = isScreenLocked();
-
-        final String lastInputMethodId = mSettings.getSelectedInputMethod();
-        int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
-        if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId);
-
         synchronized (ImfLock.class) {
+            final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
+                    && mWindowManagerInternal.isKeyguardSecure(
+                            mService.getCurrentImeUserIdLocked());
+            final String lastInputMethodId = mSettings.getSelectedInputMethod();
+            int lastInputMethodSubtypeId =
+                    mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
+            if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId);
+
             final List<ImeSubtypeListItem> imList = mSwitchingController
                     .getSortedInputMethodAndSubtypeListForImeMenuLocked(
                             showAuxSubtypes, isScreenLocked);
@@ -200,12 +202,8 @@
             mService.updateSystemUiLocked();
             mService.sendOnNavButtonFlagsChangedLocked();
             mSwitchingDialog.show();
-        }
-    }
 
-    private boolean isScreenLocked() {
-        return mWindowManagerInternal.isKeyguardLocked()
-                && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId());
+        }
     }
 
     void updateKeyboardFromSettingsLocked() {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1bdd402..707e990 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -179,6 +179,8 @@
 import android.app.role.RoleManager;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManagerInternal;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
 import android.companion.ICompanionDeviceManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
@@ -546,6 +548,15 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     static final long ENFORCE_NO_CLEAR_FLAG_ON_MEDIA_NOTIFICATION = 264179692L;
 
+    /**
+     * App calls to {@link android.app.NotificationManager#setInterruptionFilter} and
+     * {@link android.app.NotificationManager#setNotificationPolicy} manage DND through the
+     * creation and activation of an implicit {@link android.app.AutomaticZenRule}.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    static final long MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES = 308670109L;
+
     private static final Duration POST_WAKE_LOCK_TIMEOUT = Duration.ofSeconds(30);
 
     private IActivityManager mAm;
@@ -5343,6 +5354,12 @@
             if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter);
             final int callingUid = Binder.getCallingUid();
             final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi();
+
+            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+                mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen);
+                return;
+            }
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 mZenModeHelper.setManualZenMode(zen, null, pkg, "setInterruptionFilter",
@@ -5426,6 +5443,16 @@
             }
         }
 
+        private boolean canManageGlobalZenPolicy(String callingPkg, int callingUid) {
+            boolean isCompatChangeEnabled = Binder.withCleanCallingIdentity(
+                    () -> CompatChanges.isChangeEnabled(MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES,
+                            callingUid));
+            return !isCompatChangeEnabled
+                    || isCallerIsSystemOrSystemUi()
+                    || hasCompanionDevice(callingPkg, UserHandle.getUserId(callingUid),
+                            AssociationRequest.DEVICE_PROFILE_WATCH);
+        }
+
         private void enforcePolicyAccess(String pkg, String method) {
             if (PackageManager.PERMISSION_GRANTED == getContext().checkCallingPermission(
                     android.Manifest.permission.MANAGE_NOTIFICATIONS)) {
@@ -5619,6 +5646,10 @@
 
         @Override
         public Policy getNotificationPolicy(String pkg) {
+            final int callingUid = Binder.getCallingUid();
+            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+                return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(pkg);
+            }
             final long identity = Binder.clearCallingIdentity();
             try {
                 return mZenModeHelper.getNotificationPolicy();
@@ -5649,6 +5680,10 @@
             enforcePolicyAccess(pkg, "setNotificationPolicy");
             int callingUid = Binder.getCallingUid();
             boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi();
+
+            boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
+                    && !canManageGlobalZenPolicy(pkg, callingUid);
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkg,
@@ -5687,16 +5722,21 @@
                 policy = new Policy(policy.priorityCategories,
                         policy.priorityCallSenders, policy.priorityMessageSenders,
                         newVisualEffects, policy.priorityConversationSenders);
-                ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy);
-                mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi);
+
+                if (shouldApplyAsImplicitRule) {
+                    mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
+                } else {
+                    ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion,
+                            policy);
+                    mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi);
+                }
             } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set notification policy", e);
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
         }
 
-
-
         @Override
         public List<String> getEnabledNotificationListenerPackages() {
             checkCallerIsSystem();
@@ -10556,6 +10596,12 @@
     }
 
     boolean hasCompanionDevice(ManagedServiceInfo info) {
+        return hasCompanionDevice(info.component.getPackageName(),
+                info.userid, /* withDeviceProfile= */ null);
+    }
+
+    private boolean hasCompanionDevice(String pkg, @UserIdInt int userId,
+            @Nullable @AssociationRequest.DeviceProfile String withDeviceProfile) {
         if (mCompanionManager == null) {
             mCompanionManager = getCompanionManager();
         }
@@ -10565,17 +10611,19 @@
         }
         final long identity = Binder.clearCallingIdentity();
         try {
-            List<?> associations = mCompanionManager.getAssociations(
-                    info.component.getPackageName(), info.userid);
-            if (!ArrayUtils.isEmpty(associations)) {
-                return true;
+            List<AssociationInfo> associations = mCompanionManager.getAssociations(pkg, userId);
+            for (AssociationInfo association : associations) {
+                if (withDeviceProfile == null || withDeviceProfile.equals(
+                        association.getDeviceProfile())) {
+                    return true;
+                }
             }
         } catch (SecurityException se) {
             // Not a privileged listener
         } catch (RemoteException re) {
             Slog.e(TAG, "Cannot reach companion device service", re);
         } catch (Exception e) {
-            Slog.e(TAG, "Cannot verify listener " + info, e);
+            Slog.e(TAG, "Cannot verify caller pkg=" + pkg + ", userId=" + userId, e);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 8f5676b3..9106c33 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -118,7 +118,10 @@
 
     protected boolean canSnooze(int numberToSnooze) {
         synchronized (mLock) {
-            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) {
+            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT
+                    || (mPersistedSnoozedNotifications.size()
+                    + mPersistedSnoozedNotificationsWithContext.size() + numberToSnooze)
+                    > CONCURRENT_SNOOZE_LIMIT) {
                 return false;
             }
         }
@@ -357,6 +360,9 @@
 
             if (groupSummaryKey != null) {
                 NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey);
+                String trimmedKey = getTrimmedString(groupSummaryKey);
+                mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
+                mPersistedSnoozedNotifications.remove(trimmedKey);
 
                 if (record != null && !record.isCanceled) {
                     Runnable runnable = () -> {
diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java
new file mode 100644
index 0000000..2a65aff
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ZenAdapters.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import android.app.NotificationManager.Policy;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+/**
+ * Converters between different Zen representations.
+ */
+class ZenAdapters {
+
+    static ZenPolicy notificationPolicyToZenPolicy(Policy policy) {
+        ZenPolicy.Builder zenPolicyBuilder = new ZenPolicy.Builder()
+                .allowAlarms(policy.allowAlarms())
+                .allowCalls(
+                        policy.allowCalls()
+                                ? ZenModeConfig.getZenPolicySenders(policy.allowCallsFrom())
+                                : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowConversations(
+                        policy.allowConversations()
+                                ? notificationPolicyConversationSendersToZenPolicy(
+                                        policy.allowConversationsFrom())
+                                : ZenPolicy.CONVERSATION_SENDERS_NONE)
+                .allowEvents(policy.allowEvents())
+                .allowMedia(policy.allowMedia())
+                .allowMessages(
+                        policy.allowMessages()
+                                ? ZenModeConfig.getZenPolicySenders(policy.allowMessagesFrom())
+                                : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowReminders(policy.allowReminders())
+                .allowRepeatCallers(policy.allowRepeatCallers())
+                .allowSystem(policy.allowSystem());
+
+        if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
+            zenPolicyBuilder.showBadges(policy.showBadges())
+                    .showFullScreenIntent(policy.showFullScreenIntents())
+                    .showInAmbientDisplay(policy.showAmbient())
+                    .showInNotificationList(policy.showInNotificationList())
+                    .showLights(policy.showLights())
+                    .showPeeking(policy.showPeeking())
+                    .showStatusBarIcons(policy.showStatusBarIcons());
+        }
+
+        return zenPolicyBuilder.build();
+    }
+
+    @ZenPolicy.ConversationSenders
+    private static int notificationPolicyConversationSendersToZenPolicy(
+            int npPriorityConversationSenders) {
+        switch (npPriorityConversationSenders) {
+            case Policy.CONVERSATION_SENDERS_ANYONE:
+                return ZenPolicy.CONVERSATION_SENDERS_ANYONE;
+            case Policy.CONVERSATION_SENDERS_IMPORTANT:
+                return ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
+            case Policy.CONVERSATION_SENDERS_NONE:
+                return ZenPolicy.CONVERSATION_SENDERS_NONE;
+            case Policy.CONVERSATION_SENDERS_UNSET:
+            default:
+                return ZenPolicy.CONVERSATION_SENDERS_UNSET;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 762c1a1..c637df2 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -27,6 +27,7 @@
 
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
@@ -44,6 +45,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -349,11 +351,11 @@
         ZenRule rule;
         synchronized (mConfigLock) {
             if (mConfig == null) return null;
-             rule = mConfig.automaticRules.get(id);
+            rule = mConfig.automaticRules.get(id);
         }
         if (rule == null) return null;
         if (canManageAutomaticZenRule(rule)) {
-             return createAutomaticZenRule(rule);
+            return zenRuleToAutomaticZenRule(rule);
         }
         return null;
     }
@@ -439,6 +441,167 @@
         }
     }
 
+    /**
+     * Create (or activate, or deactivate) an "implicit" {@link ZenRule} when an app that has
+     * Notification Policy Access but is not allowed to manage the global zen state
+     * calls {@link NotificationManager#setInterruptionFilter}.
+     *
+     * <p>When the {@code zenMode} is {@link Global#ZEN_MODE_OFF}, an existing implicit rule will be
+     * deactivated (if there is no implicit rule, the call will be ignored). For other modes, the
+     * rule's interruption filter will match the supplied {@code zenMode}. The policy of the last
+     * call to {@link NotificationManager#setNotificationPolicy} will be used (or, if never called,
+     * the global policy).
+     *
+     * <p>The created rule is owned by the calling package, but it has neither a
+     * {@link ConditionProviderService} nor an associated
+     * {@link AutomaticZenRule#configurationActivity}.
+     *
+     * @param zenMode one of the {@code Global#ZEN_MODE_x} values
+     */
+    void applyGlobalZenModeAsImplicitZenRule(String callingPkg, int callingUid, int zenMode) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!");
+            return;
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return;
+            }
+            if (zenMode == Global.ZEN_MODE_OFF) {
+                // Deactivate implicit rule if it exists and is active; otherwise ignore.
+                ZenRule rule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
+                if (rule != null) {
+                    Condition deactivated = new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_deactivated),
+                            Condition.STATE_FALSE);
+                    setAutomaticZenRuleState(rule.id, deactivated,
+                            callingUid, /* fromSystemOrSystemUi= */ false);
+                }
+            } else {
+                // Either create a new rule with a default ZenPolicy, or update an existing rule's
+                // filter value. In both cases, also activate (and unsnooze) it.
+                ZenModeConfig newConfig = mConfig.copy();
+                ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
+                if (rule == null) {
+                    rule = newImplicitZenRule(callingPkg);
+                    newConfig.automaticRules.put(rule.id, rule);
+                }
+                rule.zenMode = zenMode;
+                rule.snoozing = false;
+                rule.condition = new Condition(rule.conditionId,
+                        mContext.getString(R.string.zen_mode_implicit_activated),
+                        Condition.STATE_TRUE);
+                setConfigLocked(newConfig, /* triggeringComponent= */ null,
+                        "applyGlobalZenModeAsImplicitZenRule",
+                        callingUid, /* fromSystemOrSystemUi= */ false);
+            }
+        }
+    }
+
+    /**
+     * Create (or update) an "implicit" {@link ZenRule} when an app that has Notification Policy
+     * Access but is not allowed to manage the global zen state calls
+     * {@link NotificationManager#setNotificationPolicy}.
+     *
+     * <p>The created rule is owned by the calling package and has the {@link ZenPolicy}
+     * corresponding to the supplied {@code policy}, but it has neither a
+     * {@link ConditionProviderService} nor an associated
+     * {@link AutomaticZenRule#configurationActivity}. Its zen mode will be set to
+     * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}.
+     */
+    void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid,
+            NotificationManager.Policy policy) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
+            return;
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return;
+            }
+            ZenModeConfig newConfig = mConfig.copy();
+            ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
+            if (rule == null) {
+                rule = newImplicitZenRule(callingPkg);
+                rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+                newConfig.automaticRules.put(rule.id, rule);
+            }
+            // TODO: b/308673679 - Keep user customization of this rule!
+            rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
+            setConfigLocked(newConfig, /* triggeringComponent= */ null,
+                    "applyGlobalPolicyAsImplicitZenRule",
+                    callingUid, /* fromSystemOrSystemUi= */ false);
+        }
+    }
+
+    /**
+     * Returns the {@link Policy} associated to the "implicit" {@link ZenRule} of a package that has
+     * Notification Policy Access but is not allowed to manage the global zen state.
+     *
+     * <p>If the implicit rule doesn't exist, or it doesn't specify a {@link ZenPolicy} (because the
+     * app never called {@link NotificationManager#setNotificationPolicy}) then the default policy
+     * is returned (i.e. same as {@link #getNotificationPolicy}.
+     *
+     * <p>Any unset values in the {@link ZenPolicy} will be mapped to their current defaults.
+     */
+    @Nullable
+    Policy getNotificationPolicyFromImplicitZenRule(String callingPkg) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!");
+            return getNotificationPolicy();
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return null;
+            }
+            ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
+            if (implicitRule != null && implicitRule.zenPolicy != null) {
+                return mConfig.toNotificationPolicy(implicitRule.zenPolicy);
+            } else {
+                return getNotificationPolicy();
+            }
+        }
+    }
+
+    /**
+     * Creates an empty {@link ZenRule} to be used as the implicit rule for {@code pkg}.
+     * Both {@link ZenRule#zenMode} and {@link ZenRule#zenPolicy} are unset.
+     */
+    private ZenRule newImplicitZenRule(String pkg) {
+        ZenRule rule = new ZenRule();
+        rule.id = implicitRuleId(pkg);
+        rule.pkg = pkg;
+        rule.creationTime = System.currentTimeMillis();
+
+        Binder.withCleanCallingIdentity(() -> {
+            try {
+                ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0);
+                rule.name = applicationInfo.loadLabel(mPm).toString();
+            } catch (PackageManager.NameNotFoundException e) {
+                // Should not happen, since it's the app calling us (?)
+                Log.w(TAG, "Package not found for creating implicit zen rule");
+                rule.name = "Unknown";
+            }
+        });
+
+        rule.condition = null;
+        rule.conditionId = new Uri.Builder()
+                .scheme(Condition.SCHEME)
+                .authority("android")
+                .appendPath("implicit")
+                .appendPath(pkg)
+                .build();
+        rule.enabled = true;
+        rule.modified = false;
+        rule.component = null;
+        rule.configurationActivity = null;
+        return rule;
+    }
+
+    private static String implicitRuleId(String forPackage) {
+        return "implicit_" + forPackage;
+    }
+
     public boolean removeAutomaticZenRule(String id, String reason, int callingUid,
             boolean fromSystemOrSystemUi) {
         ZenModeConfig newConfig;
@@ -626,7 +789,7 @@
                         }
                         // update default rule (if locale changed, name of rule will change)
                         currRule.name = defaultRule.name;
-                        updateAutomaticZenRule(defaultRule.id, createAutomaticZenRule(currRule),
+                        updateAutomaticZenRule(defaultRule.id, zenRuleToAutomaticZenRule(currRule),
                                 "locale changed", callingUid, fromSystemOrSystemUi);
                     }
                 }
@@ -669,7 +832,7 @@
         return null;
     }
 
-    private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
+    private static void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
             boolean isNew) {
         if (rule.enabled != automaticZenRule.isEnabled()) {
             rule.snoozing = false;
@@ -699,7 +862,7 @@
         }
     }
 
-    protected AutomaticZenRule createAutomaticZenRule(ZenRule rule) {
+    private static AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
         AutomaticZenRule azr;
         if (Flags.modesApi()) {
             azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId)
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 25b7ca1..dcac8c9 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -7,8 +7,6 @@
   bug: "290381858"
 }
 
-
-
 flag {
   name: "polite_notifications"
   namespace: "systemui"
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index cba605e..2617703 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -60,6 +60,7 @@
 import static com.android.server.pm.PackageManagerServiceUtils.compareSignatureArrays;
 import static com.android.server.pm.PackageManagerServiceUtils.compareSignatures;
 import static com.android.server.pm.PackageManagerServiceUtils.isSystemOrRootOrShell;
+import static com.android.server.pm.parsing.PackageInfoUtils.getDeprecatedSignatures;
 import static com.android.server.pm.resolution.ComponentResolver.RESOLVE_PRIORITY_SORTER;
 
 import android.Manifest;
@@ -1531,6 +1532,7 @@
             pi.applicationInfo = PackageInfoUtils.generateDelegateApplicationInfo(
                     ai, flags, state, userId);
             pi.signingInfo = ps.getSigningInfo();
+            pi.signatures = getDeprecatedSignatures(pi.signingInfo.getSigningDetails(), flags);
 
             if (DEBUG_PACKAGE_INFO) {
                 Log.v(TAG, "ps.pkg is n/a for ["
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index c26cf1c..38cde3e 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -238,21 +238,7 @@
         }
 
         final SigningDetails signingDetails = pkg.getSigningDetails();
-        // deprecated method of getting signing certificates
-        if ((flags & PackageManager.GET_SIGNATURES) != 0) {
-            if (signingDetails.hasPastSigningCertificates()) {
-                // Package has included signing certificate rotation information.  Return the oldest
-                // cert so that programmatic checks keep working even if unaware of key rotation.
-                info.signatures = new Signature[1];
-                info.signatures[0] = signingDetails.getPastSigningCertificates()[0];
-            } else if (signingDetails.hasSignatures()) {
-                // otherwise keep old behavior
-                int numberOfSigs = signingDetails.getSignatures().length;
-                info.signatures = new Signature[numberOfSigs];
-                System.arraycopy(signingDetails.getSignatures(), 0, info.signatures, 0,
-                        numberOfSigs);
-            }
-        }
+        info.signatures = getDeprecatedSignatures(signingDetails, flags);
 
         // replacement for GET_SIGNATURES
         if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) {
@@ -358,6 +344,30 @@
         return info;
     }
 
+    /**
+     *  Retrieve the deprecated {@link PackageInfo.signatures} field of signing certificates
+     */
+    public static Signature[] getDeprecatedSignatures(SigningDetails signingDetails, long flags) {
+        if ((flags & PackageManager.GET_SIGNATURES) == 0) {
+            return null;
+        }
+        if (signingDetails.hasPastSigningCertificates()) {
+            // Package has included signing certificate rotation information.  Return the oldest
+            // cert so that programmatic checks keep working even if unaware of key rotation.
+            Signature[] signatures = new Signature[1];
+            signatures[0] = signingDetails.getPastSigningCertificates()[0];
+            return signatures;
+        } else if (signingDetails.hasSignatures()) {
+            // otherwise keep old behavior
+            int numberOfSigs = signingDetails.getSignatures().length;
+            Signature[] signatures = new Signature[numberOfSigs];
+            System.arraycopy(signingDetails.getSignatures(), 0, signatures, 0,
+                    numberOfSigs);
+            return signatures;
+        }
+        return null;
+    }
+
     private static void updateApplicationInfo(ApplicationInfo ai, long flags,
             PackageUserState state) {
         if ((flags & PackageManager.GET_META_DATA) == 0) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 1b5631f5..a2f5a38 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -256,13 +256,24 @@
             mOriginatingPendingIntent = originatingPendingIntent;
             mIntent = intent;
             mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+            if (originatingPendingIntent == null) {
+                // grant creator BAL privileges unless explicitly opted out
+                mBalAllowedByPiCreator =
+                        checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
+                                ? BackgroundStartPrivileges.NONE
+                                : BackgroundStartPrivileges.ALLOW_BAL;
+            } else {
+                // for PendingIntents we restrict creator BAL based on target_sdk
+                mBalAllowedByPiCreator =
+                        checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
+                                ? BackgroundStartPrivileges.NONE
+                                : BackgroundStartPrivileges.ALLOW_BAL;
+            }
             mBalAllowedByPiSender =
-                    PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(checkedOptions,
-                            realCallingUid, mRealCallingPackage);
-            mBalAllowedByPiCreator =
-                    checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
-                            == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
-                            ? BackgroundStartPrivileges.NONE : BackgroundStartPrivileges.ALLOW_BAL;
+                    PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
+                            checkedOptions, realCallingUid, mRealCallingPackage);
             mAppSwitchState = mService.getBalAppSwitchesState();
             mCallingUidProcState = mService.mActiveUids.getUidState(callingUid);
             mIsCallingUidPersistentSystemProcess =
@@ -306,10 +317,14 @@
             return name + "[debugOnly]";
         }
 
-        private boolean isPendingIntent() {
+        private boolean hasRealCaller() {
             return mRealCallingUid != NO_PROCESS_UID;
         }
 
+        private boolean isPendingIntent() {
+            return mOriginatingPendingIntent != null;
+        }
+
         private String dump(BalVerdict resultIfPiCreatorAllowsBal) {
             Preconditions.checkState(!isPendingIntent());
             return dump(resultIfPiCreatorAllowsBal, null);
@@ -334,7 +349,9 @@
             sb.append("; isCallingUidPersistentSystemProcess: ")
                     .append(mIsCallingUidPersistentSystemProcess);
             sb.append("; balAllowedByPiCreator: ").append(mBalAllowedByPiCreator);
-            if (isPendingIntent()) {
+            sb.append("; hasRealCaller: ").append(hasRealCaller());
+            sb.append("; isPendingIntent: ").append(isPendingIntent());
+            if (hasRealCaller()) {
                 sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender);
                 sb.append("; realCallingPackage: ")
                         .append(getDebugPackageName(mRealCallingPackage, mRealCallingUid));
@@ -351,13 +368,13 @@
             sb.append("; mForcedBalByPiSender: ").append(mForcedBalByPiSender);
             sb.append("; intent: ").append(mIntent);
             sb.append("; callerApp: ").append(mCallerApp);
-            if (isPendingIntent()) {
+            if (hasRealCaller()) {
                 sb.append("; realCallerApp: ").append(mRealCallerApp);
             }
             if (mCallerApp != null) {
                 sb.append("; inVisibleTask: ").append(mCallerApp.hasActivityInVisibleTask());
             }
-            if (isPendingIntent()) {
+            if (hasRealCaller()) {
                 if (mRealCallerApp != null) {
                     sb.append("; realInVisibleTask: ")
                             .append(mRealCallerApp.hasActivityInVisibleTask());
@@ -484,7 +501,7 @@
 
         BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state);
 
-        if (!state.isPendingIntent()) {
+        if (!state.hasRealCaller()) {
             if (resultForCaller.allows()) {
                 if (DEBUG_ACTIVITY_STARTS) {
                     Slog.d(TAG, "Background activity start allowed. "
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a778415..c9703db 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4648,7 +4648,7 @@
                                 // Expanding pip into new rotation, so create a rotation leash
                                 // until the display is rotated.
                                 topActivity.getOrCreateFixedRotationLeash(
-                                        topActivity.getSyncTransaction());
+                                        topActivity.getPendingTransaction());
                             }
                             lastParentBeforePip.moveToFront("movePinnedActivityToOriginalTask");
                         }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c0bf2ce..3e23fab 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1052,12 +1052,12 @@
      * @return true if we are *guaranteed* to enter-pip. This means we return false if there's
      *         a chance we won't thus legacy-entry (via pause+userLeaving) will return false.
      */
-    private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar,
-            @Nullable ActivityRecord resuming) {
+    private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar) {
         if (!mCanPipOnFinish || !ar.isVisible() || ar.getTask() == null || !ar.isState(RESUMED)) {
             return false;
         }
 
+        final ActivityRecord resuming = getVisibleTransientLaunch(ar.getTaskDisplayArea());
         if (ar.pictureInPictureArgs != null && ar.pictureInPictureArgs.isAutoEnterEnabled()) {
             if (!ar.getTask().isVisibleRequested() || didCommitTransientLaunch()) {
                 // force enable pip-on-task-switch now that we've committed to actually launching
@@ -1196,9 +1196,7 @@
                 final boolean isScreenOff = ar.mDisplayContent == null
                         || ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF;
                 if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) {
-                    final ActivityRecord resuming = getVisibleTransientLaunch(
-                            ar.getTaskDisplayArea());
-                    final boolean commitVisibility = !checkEnterPipOnFinish(ar, resuming);
+                    final boolean commitVisibility = !checkEnterPipOnFinish(ar);
                     // Avoid commit visibility if entering pip or else we will get a sudden
                     // "flash" / surface going invisible for a split second.
                     if (commitVisibility) {
@@ -1431,7 +1429,7 @@
             if (candidateActivity.getTaskDisplayArea() != taskDisplayArea) {
                 continue;
             }
-            if (!candidateActivity.isVisible()) {
+            if (!candidateActivity.isVisibleRequested()) {
                 continue;
             }
             return candidateActivity;
diff --git a/services/core/java/com/android/server/wm/WindowManagerFlags.java b/services/core/java/com/android/server/wm/WindowManagerFlags.java
index 4667710..b3a3650 100644
--- a/services/core/java/com/android/server/wm/WindowManagerFlags.java
+++ b/services/core/java/com/android/server/wm/WindowManagerFlags.java
@@ -49,5 +49,8 @@
 
     final boolean mWallpaperOffsetAsync = Flags.wallpaperOffsetAsync();
 
+    final boolean mAllowsScreenSizeDecoupledFromStatusBarAndCutout =
+            Flags.allowsScreenSizeDecoupledFromStatusBarAndCutout();
+
     /* End Available Flags */
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 55678c5..0a986c8 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -336,7 +336,6 @@
 import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
 import com.android.server.power.ShutdownThread;
 import com.android.server.utils.PriorityDump;
-import com.android.window.flags.Flags;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -1194,7 +1193,7 @@
                 .getBoolean(R.bool.config_skipActivityRelaunchWhenDocking);
         final boolean isScreenSizeDecoupledFromStatusBarAndCutout = context.getResources()
                 .getBoolean(R.bool.config_decoupleStatusBarAndDisplayCutoutFromScreenSize)
-                && Flags.closeToSquareConfigIncludesStatusBar();
+                && mFlags.mAllowsScreenSizeDecoupledFromStatusBarAndCutout;
         if (!isScreenSizeDecoupledFromStatusBarAndCutout) {
             mDecorTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars();
             mConfigTypes = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp
index 80427b3..505421e 100644
--- a/services/core/jni/tvinput/JTvInputHal.cpp
+++ b/services/core/jni/tvinput/JTvInputHal.cpp
@@ -494,12 +494,21 @@
 ::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::setCallback(
         const std::shared_ptr<TvInputCallbackWrapper>& in_callback) {
     if (mIsHidl) {
-        in_callback->aidlTvInputCallback = nullptr;
-        return hidlSetCallback(in_callback == nullptr ? nullptr : in_callback->hidlTvInputCallback);
+        if (in_callback == nullptr) {
+            return hidlSetCallback(nullptr);
+        }
+        else {
+            in_callback->aidlTvInputCallback = nullptr;
+            return hidlSetCallback(in_callback->hidlTvInputCallback);
+        }
     } else {
-        in_callback->hidlTvInputCallback = nullptr;
-        return mAidlTvInput->setCallback(in_callback == nullptr ? nullptr
-                                                                : in_callback->aidlTvInputCallback);
+        if (in_callback == nullptr) {
+            return mAidlTvInput->setCallback(nullptr);
+        }
+        else {
+            in_callback->hidlTvInputCallback = nullptr;
+            return mAidlTvInput->setCallback(in_callback->aidlTvInputCallback);
+        }
     }
 }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 34d6755..9b62a2c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -9667,6 +9667,26 @@
         }
     }
 
+    @Override
+    public ComponentName getDeviceOwnerComponentOnUser(int userId) {
+        if (!mHasFeature) {
+            return null;
+        }
+        if (mInjector.userHandleGetCallingUserId() != userId) {
+            Preconditions.checkCallAuthorization(canManageUsers(getCallerIdentity())
+                    || hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS));
+        }
+        synchronized (getLockObject()) {
+            // There is only ever one device owner on a device so if the passed userId is the same
+            // as the device owner userId we know that the componentName returned by
+            // getDeviceOwnerComponent will be the correct one.
+            if (mOwners.getDeviceOwnerUserId() == userId || userId == UserHandle.USER_ALL) {
+                return mOwners.getDeviceOwnerComponent();
+            }
+        }
+        return null;
+    }
+
     private int getDeviceOwnerUserIdUncheckedLocked() {
         return mOwners.hasDeviceOwner() ? mOwners.getDeviceOwnerUserId() : UserHandle.USER_NULL;
     }
diff --git a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
index 15a5859..7db09f9 100644
--- a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
@@ -16,7 +16,9 @@
 
 package com.android.server.permission.access.permission
 
+import android.Manifest
 import android.permission.PermissionManager
+import android.permission.flags.Flags
 import android.util.Slog
 import com.android.modules.utils.BinaryXmlPullParser
 import com.android.modules.utils.BinaryXmlSerializer
@@ -273,7 +275,12 @@
 
         /** These permissions are supported for virtual devices. */
         // TODO: b/298661870 - Use new API to get the list of device aware permissions.
-        val DEVICE_AWARE_PERMISSIONS = emptySet<String>()
+        val DEVICE_AWARE_PERMISSIONS =
+            if (Flags.deviceAwarePermissionApis()) {
+                setOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
+            } else {
+                emptySet<String>()
+            }
     }
 
     /**
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
new file mode 100644
index 0000000..72dc725
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.annotation.NonNull;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.TestLooperManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.SparseArray;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.AlarmManagerInternal;
+import com.android.server.DropBoxManagerInternal;
+import com.android.server.LocalServices;
+import com.android.server.appop.AppOpsService;
+import com.android.server.wm.ActivityTaskManagerService;
+
+import org.junit.Rule;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public abstract class BaseBroadcastQueueTest {
+
+    static final int USER_GUEST = 11;
+
+    static final String PACKAGE_ANDROID = "android";
+    static final String PACKAGE_PHONE = "com.android.phone";
+    static final String PACKAGE_RED = "com.example.red";
+    static final String PACKAGE_GREEN = "com.example.green";
+    static final String PACKAGE_BLUE = "com.example.blue";
+    static final String PACKAGE_YELLOW = "com.example.yellow";
+    static final String PACKAGE_ORANGE = "com.example.orange";
+
+    static final String PROCESS_SYSTEM = "system";
+
+    static final String CLASS_RED = "com.example.red.Red";
+    static final String CLASS_GREEN = "com.example.green.Green";
+    static final String CLASS_BLUE = "com.example.blue.Blue";
+    static final String CLASS_YELLOW = "com.example.yellow.Yellow";
+    static final String CLASS_ORANGE = "com.example.orange.Orange";
+
+    static final BroadcastProcessQueue.BroadcastPredicate BROADCAST_PREDICATE_ANY =
+            (r, i) -> true;
+
+    @Rule
+    public final ApplicationExitInfoTest.ServiceThreadRule
+            mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
+
+    @Mock
+    AppOpsService mAppOpsService;
+    @Mock
+    PackageManagerInternal mPackageManagerInt;
+    @Mock
+    UsageStatsManagerInternal mUsageStatsManagerInt;
+    @Mock
+    DropBoxManagerInternal mDropBoxManagerInt;
+    @Mock
+    AlarmManagerInternal mAlarmManagerInt;
+    @Mock
+    ProcessList mProcessList;
+
+    Context mContext;
+    ActivityManagerService mAms;
+    BroadcastConstants mConstants;
+    BroadcastSkipPolicy mSkipPolicy;
+    HandlerThread mHandlerThread;
+    TestLooperManager mLooper;
+    AtomicInteger mNextPid;
+
+    /**
+     * Map from PID to registered registered runtime receivers.
+     */
+    SparseArray<ReceiverList> mRegisteredReceivers = new SparseArray<>();
+
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mHandlerThread = new HandlerThread(getTag());
+        mHandlerThread.start();
+        // Pause all event processing until a test chooses to resume
+        mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation()
+                .acquireLooperManager(mHandlerThread.getLooper()));
+        mNextPid = new AtomicInteger(100);
+
+        LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
+        LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+        LocalServices.removeServiceForTest(AlarmManagerInternal.class);
+        LocalServices.addService(AlarmManagerInternal.class, mAlarmManagerInt);
+        doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
+        doNothing().when(mPackageManagerInt).notifyComponentUsed(any(), anyInt(), any(), any());
+        doAnswer((invocation) -> {
+            return getUidForPackage(invocation.getArgument(0));
+        }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
+
+        final ActivityManagerService realAms = new ActivityManagerService(
+                new TestInjector(mContext), mServiceThreadRule.getThread());
+        realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
+        realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
+        realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
+        realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
+        realAms.mPackageManagerInt = mPackageManagerInt;
+        realAms.mUsageStatsService = mUsageStatsManagerInt;
+        realAms.mProcessesReady = true;
+        mAms = spy(realAms);
+
+        mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
+        doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
+        doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
+
+        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+    }
+
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+    }
+
+    static int getUidForPackage(@NonNull String packageName) {
+        switch (packageName) {
+            case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
+            case PACKAGE_PHONE: return android.os.Process.PHONE_UID;
+            case PACKAGE_RED: return android.os.Process.FIRST_APPLICATION_UID + 1;
+            case PACKAGE_GREEN: return android.os.Process.FIRST_APPLICATION_UID + 2;
+            case PACKAGE_BLUE: return android.os.Process.FIRST_APPLICATION_UID + 3;
+            case PACKAGE_YELLOW: return android.os.Process.FIRST_APPLICATION_UID + 4;
+            case PACKAGE_ORANGE: return android.os.Process.FIRST_APPLICATION_UID + 5;
+            default: throw new IllegalArgumentException();
+        }
+    }
+
+    static int getUidForPackage(@NonNull String packageName, int userId) {
+        return UserHandle.getUid(userId, getUidForPackage(packageName));
+    }
+
+    private class TestInjector extends ActivityManagerService.Injector {
+        TestInjector(Context context) {
+            super(context);
+        }
+
+        @Override
+        public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile,
+                                              Handler handler) {
+            return mAppOpsService;
+        }
+
+        @Override
+        public Handler getUiHandler(ActivityManagerService service) {
+            return mHandlerThread.getThreadHandler();
+        }
+
+        @Override
+        public ProcessList getProcessList(ActivityManagerService service) {
+            return mProcessList;
+        }
+    }
+
+    abstract String getTag();
+
+    static ApplicationInfo makeApplicationInfo(String packageName) {
+        return makeApplicationInfo(packageName, packageName, UserHandle.USER_SYSTEM);
+    }
+
+    static ApplicationInfo makeApplicationInfo(String packageName, String processName, int userId) {
+        final ApplicationInfo ai = new ApplicationInfo();
+        ai.packageName = packageName;
+        ai.processName = processName;
+        ai.uid = getUidForPackage(packageName, userId);
+        return ai;
+    }
+
+    static ResolveInfo withPriority(ResolveInfo info, int priority) {
+        info.priority = priority;
+        return info;
+    }
+
+    static BroadcastFilter withPriority(BroadcastFilter filter, int priority) {
+        filter.setPriority(priority);
+        return filter;
+    }
+
+    static ResolveInfo makeManifestReceiver(String packageName, String name) {
+        return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
+    }
+
+    static ResolveInfo makeManifestReceiver(String packageName, String name, int userId) {
+        return makeManifestReceiver(packageName, packageName, name, userId);
+    }
+
+    static ResolveInfo makeManifestReceiver(String packageName, String processName,
+            String name, int userId) {
+        final ResolveInfo ri = new ResolveInfo();
+        ri.activityInfo = new ActivityInfo();
+        ri.activityInfo.packageName = packageName;
+        ri.activityInfo.processName = processName;
+        ri.activityInfo.name = name;
+        ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
+        return ri;
+    }
+
+    BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
+        return makeRegisteredReceiver(app, 0);
+    }
+
+    BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
+        final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
+        return makeRegisteredReceiver(receiverList, priority);
+    }
+
+    static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) {
+        final IntentFilter filter = new IntentFilter();
+        filter.setPriority(priority);
+        final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
+                receiverList.app.info.packageName, null, null, null, receiverList.uid,
+                receiverList.userId, false, false, true);
+        receiverList.add(res);
+        return res;
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 08f5d03..2378416 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -31,17 +31,6 @@
 import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_PRIORITIZED;
 import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList;
 import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList;
-import static com.android.server.am.BroadcastQueueTest.CLASS_BLUE;
-import static com.android.server.am.BroadcastQueueTest.CLASS_GREEN;
-import static com.android.server.am.BroadcastQueueTest.CLASS_RED;
-import static com.android.server.am.BroadcastQueueTest.CLASS_YELLOW;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_BLUE;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_GREEN;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_RED;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_YELLOW;
-import static com.android.server.am.BroadcastQueueTest.getUidForPackage;
-import static com.android.server.am.BroadcastQueueTest.makeManifestReceiver;
-import static com.android.server.am.BroadcastQueueTest.withPriority;
 import static com.android.server.am.BroadcastRecord.isReceiverEquals;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -74,17 +63,15 @@
 import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.BundleMerger;
 import android.os.DropBoxManager;
-import android.os.HandlerThread;
 import android.os.Process;
 import android.os.SystemClock;
-import android.os.TestLooperManager;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
 
@@ -108,11 +95,12 @@
 import java.util.Objects;
 
 @SmallTest
-public final class BroadcastQueueModernImplTest {
+public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest {
+    private static final String TAG = "BroadcastQueueModernImplTest";
+
     private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID;
     private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1;
 
-    @Mock ActivityManagerService mAms;
     @Mock ProcessRecord mProcess;
 
     @Mock BroadcastProcessQueue mQueue1;
@@ -120,11 +108,6 @@
     @Mock BroadcastProcessQueue mQueue3;
     @Mock BroadcastProcessQueue mQueue4;
 
-    HandlerThread mHandlerThread;
-    TestLooperManager mLooper;
-
-    BroadcastConstants mConstants;
-    private BroadcastSkipPolicy mSkipPolicy;
     BroadcastQueueModernImpl mImpl;
 
     BroadcastProcessQueue mHead;
@@ -136,22 +119,12 @@
 
     @Before
     public void setUp() throws Exception {
-        mHandlerThread = new HandlerThread(getClass().getSimpleName());
-        mHandlerThread.start();
+        super.setUp();
 
-        // Pause all event processing until a test chooses to resume
-        mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation()
-                .acquireLooperManager(mHandlerThread.getLooper()));
-
-        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
         mConstants.DELAY_URGENT_MILLIS = -120_000;
         mConstants.DELAY_NORMAL_MILLIS = 10_000;
         mConstants.DELAY_CACHED_MILLIS = 120_000;
 
-        mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
-        doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
-        doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
-
         final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
             public void addBroadcastToHistoryLocked(BroadcastRecord original) {
                 // Ignored
@@ -169,7 +142,12 @@
 
     @After
     public void tearDown() throws Exception {
-        mHandlerThread.quit();
+        super.tearDown();
+    }
+
+    @Override
+    public String getTag() {
+        return TAG;
     }
 
     /**
@@ -225,11 +203,6 @@
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), false);
     }
 
-    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent) {
-        return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(),
-                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), true);
-    }
-
     private BroadcastRecord makeBroadcastRecord(Intent intent, List receivers) {
         return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(), receivers, false);
     }
@@ -246,8 +219,8 @@
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options,
             List receivers, IIntentReceiver resultTo, boolean ordered) {
-        return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, 42, false, null,
-                null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo,
+        return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, TEST_UID, false,
+                null, null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo,
                 Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM,
                 BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN);
     }
@@ -259,12 +232,12 @@
 
     private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
             BroadcastRecord record, int recordIndex, long enqueueTime) {
-        queue.enqueueOrReplaceBroadcast(record, recordIndex, (r, i) -> {
-            throw new UnsupportedOperationException();
-        });
         record.enqueueTime = enqueueTime;
         record.enqueueRealTime = enqueueTime;
         record.enqueueClockTime = enqueueTime;
+        queue.enqueueOrReplaceBroadcast(record, recordIndex, (r, i) -> {
+            throw new UnsupportedOperationException();
+        });
     }
 
     @Test
@@ -419,6 +392,7 @@
         assertFalse(queue.isRunnable());
         assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
                 queue.getRunnableAtReason());
+        assertTrue(queue.shouldBeDeferred());
         assertEquals(ProcessList.SCHED_GROUP_UNDEFINED, queue.getPreferredSchedulingGroupLocked());
     }
 
@@ -445,6 +419,7 @@
         assertThat(cachedRunnableAt).isGreaterThan(notCachedRunnableAt);
         assertTrue(queue.isRunnable());
         assertEquals(BroadcastProcessQueue.REASON_CACHED, queue.getRunnableAtReason());
+        assertTrue(queue.shouldBeDeferred());
         assertEquals(ProcessList.SCHED_GROUP_UNDEFINED, queue.getPreferredSchedulingGroupLocked());
     }
 
@@ -526,11 +501,13 @@
         queue.invalidateRunnableAt();
         assertThat(queue.getRunnableAt()).isGreaterThan(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         mConstants.MAX_PENDING_BROADCASTS = 1;
         queue.invalidateRunnableAt();
         assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_MAX_PENDING, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -549,10 +526,12 @@
         queue.setProcessAndUidState(mProcess, true, false);
         assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_FOREGROUND, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -570,6 +549,7 @@
 
         assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_TOP_PROCESS, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         doReturn(ActivityManager.PROCESS_STATE_SERVICE).when(mProcess).getSetProcState();
         queue.setProcessAndUidState(mProcess, false, false);
@@ -580,6 +560,7 @@
                 List.of(makeMockRegisteredReceiver())), 0);
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -594,16 +575,19 @@
 
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         doReturn(true).when(mProcess).isPersistent();
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_PERSISTENT, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         doReturn(false).when(mProcess).isPersistent();
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -618,6 +602,7 @@
 
         assertThat(queue.getRunnableAt()).isEqualTo(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_CORE_UID, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -636,10 +621,12 @@
         assertEquals(Long.MAX_VALUE, queue.getRunnableAt());
         assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
                 queue.getRunnableAtReason());
+        assertTrue(queue.shouldBeDeferred());
 
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isEqualTo(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_CORE_UID, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     /**
@@ -1575,6 +1562,216 @@
         verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
     }
 
+    @Test
+    public void testDeliveryDeferredForCached() throws Exception {
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    @Test
+    public void testDeliveryDeferredForCached_withInfiniteDeferred() throws Exception {
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastOptions optionsTimeTick = BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, optionsTimeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastOptions optionsPackageChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                optionsPackageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
+    private ProcessRecord makeProcessRecord(ApplicationInfo info) {
+        final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
+        r.setPid(mNextPid.incrementAndGet());
+        return r;
+    }
+
+    BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
+        final IIntentReceiver receiver = mock(IIntentReceiver.class);
+        final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
+                UserHandle.getUserId(app.info.uid), receiver);
+        return makeRegisteredReceiver(receiverList, priority);
+    }
+
     private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
         final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
         packageChangedIntent.putExtra(Intent.EXTRA_UID, uid);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 1c8e949..3364545 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -62,58 +62,36 @@
 import android.app.IApplicationThread;
 import android.app.UidObserver;
 import android.app.usage.UsageEvents.Event;
-import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.IIntentReceiver;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
-import android.content.pm.ResolveInfo;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.DeadObjectException;
-import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.PowerExemptionManager;
 import android.os.SystemClock;
-import android.os.TestLooperManager;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.Log;
 import android.util.Pair;
-import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.server.AlarmManagerInternal;
-import com.android.server.DropBoxManagerInternal;
-import com.android.server.LocalServices;
-import com.android.server.am.ActivityManagerService.Injector;
-import com.android.server.appop.AppOpsService;
-import com.android.server.wm.ActivityTaskManagerService;
-
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 import org.mockito.ArgumentMatcher;
 import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 import org.mockito.verification.VerificationMode;
 
-import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.io.Writer;
@@ -126,7 +104,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.UnaryOperator;
 
@@ -136,13 +113,9 @@
 @MediumTest
 @RunWith(Parameterized.class)
 @SuppressWarnings("GuardedBy")
-public class BroadcastQueueTest {
+public class BroadcastQueueTest extends BaseBroadcastQueueTest {
     private static final String TAG = "BroadcastQueueTest";
 
-    @Rule
-    public final ApplicationExitInfoTest.ServiceThreadRule
-            mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
-
     private final Impl mImpl;
 
     private enum Impl {
@@ -150,30 +123,8 @@
         MODERN,
     }
 
-    private Context mContext;
-    private HandlerThread mHandlerThread;
-    private TestLooperManager mLooper;
-    private AtomicInteger mNextPid;
-
-    @Mock
-    private AppOpsService mAppOpsService;
-    @Mock
-    private ProcessList mProcessList;
-    @Mock
-    private DropBoxManagerInternal mDropBoxManagerInt;
-    @Mock
-    private PackageManagerInternal mPackageManagerInt;
-    @Mock
-    private UsageStatsManagerInternal mUsageStatsManagerInt;
-    @Mock
-    private AlarmManagerInternal mAlarmManagerInt;
-
-    private ActivityManagerService mAms;
     private BroadcastQueue mQueue;
-    BroadcastConstants mConstants;
-    private BroadcastSkipPolicy mSkipPolicy;
     private UidObserver mUidObserver;
-    private UidObserver mUidCachedStateObserver;
 
     /**
      * Desired behavior of the next
@@ -183,11 +134,6 @@
             ProcessStartBehavior.SUCCESS);
 
     /**
-     * Map from PID to registered registered runtime receivers.
-     */
-    private SparseArray<ReceiverList> mRegisteredReceivers = new SparseArray<>();
-
-    /**
      * Collection of all active processes during current test run.
      */
     private List<ProcessRecord> mActiveProcesses = new ArrayList<>();
@@ -208,41 +154,8 @@
 
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
+        super.setUp();
 
-        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
-        mHandlerThread = new HandlerThread(TAG);
-        mHandlerThread.start();
-
-        // Pause all event processing until a test chooses to resume
-        mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation()
-                .acquireLooperManager(mHandlerThread.getLooper()));
-
-        mNextPid = new AtomicInteger(100);
-
-        LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
-        LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
-        LocalServices.removeServiceForTest(PackageManagerInternal.class);
-        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
-        LocalServices.removeServiceForTest(AlarmManagerInternal.class);
-        LocalServices.addService(AlarmManagerInternal.class, mAlarmManagerInt);
-        doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
-        doNothing().when(mPackageManagerInt).notifyComponentUsed(any(), anyInt(), any(), any());
-        doAnswer((invocation) -> {
-            return getUidForPackage(invocation.getArgument(0));
-        }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
-
-        final ActivityManagerService realAms = new ActivityManagerService(
-                new TestInjector(mContext), mServiceThreadRule.getThread());
-        realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
-        realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
-        realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
-        realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
-        realAms.mPackageManagerInt = mPackageManagerInt;
-        realAms.mUsageStatsService = mUsageStatsManagerInt;
-        realAms.mProcessesReady = true;
-        mAms = spy(realAms);
         doAnswer((invocation) -> {
             Log.v(TAG, "Intercepting startProcessLocked() for "
                     + Arrays.toString(invocation.getArguments()));
@@ -321,21 +234,11 @@
             return null;
         }).when(mAms).registerUidObserver(any(), anyInt(),
                 eq(ActivityManager.PROCESS_STATE_TOP), any());
-        doAnswer((invocation) -> {
-            mUidCachedStateObserver = invocation.getArgument(0);
-            return null;
-        }).when(mAms).registerUidObserver(any(), anyInt(),
-                eq(ActivityManager.PROCESS_STATE_LAST_ACTIVITY), any());
 
-        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
         mConstants.TIMEOUT = 200;
         mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
         mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
 
-        mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
-        doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
-        doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
-
         final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
             public void addBroadcastToHistoryLocked(BroadcastRecord original) {
                 // Ignored
@@ -358,7 +261,7 @@
 
     @After
     public void tearDown() throws Exception {
-        mHandlerThread.quit();
+        super.tearDown();
 
         // Verify that all processes have finished handling broadcasts
         for (ProcessRecord app : mActiveProcesses) {
@@ -369,26 +272,9 @@
         }
     }
 
-    private class TestInjector extends Injector {
-        TestInjector(Context context) {
-            super(context);
-        }
-
-        @Override
-        public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile,
-                Handler handler) {
-            return mAppOpsService;
-        }
-
-        @Override
-        public Handler getUiHandler(ActivityManagerService service) {
-            return mHandlerThread.getThreadHandler();
-        }
-
-        @Override
-        public ProcessList getProcessList(ActivityManagerService service) {
-            return mProcessList;
-        }
+    @Override
+    public String getTag() {
+        return TAG;
     }
 
     private enum ProcessStartBehavior {
@@ -534,62 +420,6 @@
         return Pair.create(app.getPid(), intent.getAction());
     }
 
-    static ApplicationInfo makeApplicationInfo(String packageName) {
-        return makeApplicationInfo(packageName, packageName, UserHandle.USER_SYSTEM);
-    }
-
-    static ApplicationInfo makeApplicationInfo(String packageName, String processName, int userId) {
-        final ApplicationInfo ai = new ApplicationInfo();
-        ai.packageName = packageName;
-        ai.processName = processName;
-        ai.uid = getUidForPackage(packageName, userId);
-        return ai;
-    }
-
-    static ResolveInfo withPriority(ResolveInfo info, int priority) {
-        info.priority = priority;
-        return info;
-    }
-
-    static BroadcastFilter withPriority(BroadcastFilter filter, int priority) {
-        filter.setPriority(priority);
-        return filter;
-    }
-
-    static ResolveInfo makeManifestReceiver(String packageName, String name) {
-        return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
-    }
-
-    static ResolveInfo makeManifestReceiver(String packageName, String name, int userId) {
-        return makeManifestReceiver(packageName, packageName, name, userId);
-    }
-
-    static ResolveInfo makeManifestReceiver(String packageName, String processName, String name,
-            int userId) {
-        final ResolveInfo ri = new ResolveInfo();
-        ri.activityInfo = new ActivityInfo();
-        ri.activityInfo.packageName = packageName;
-        ri.activityInfo.processName = processName;
-        ri.activityInfo.name = name;
-        ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
-        return ri;
-    }
-
-    private BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
-        return makeRegisteredReceiver(app, 0);
-    }
-
-    private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
-        final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
-        final IntentFilter filter = new IntentFilter();
-        filter.setPriority(priority);
-        final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
-                receiverList.app.info.packageName, null, null, null, receiverList.uid,
-                receiverList.userId, false, false, true);
-        receiverList.add(res);
-        return res;
-    }
-
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             List<Object> receivers) {
         return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
@@ -776,41 +606,6 @@
                 eq(userId), anyInt(), anyInt(), any());
     }
 
-    static final int USER_GUEST = 11;
-
-    static final String PACKAGE_ANDROID = "android";
-    static final String PACKAGE_PHONE = "com.android.phone";
-    static final String PACKAGE_RED = "com.example.red";
-    static final String PACKAGE_GREEN = "com.example.green";
-    static final String PACKAGE_BLUE = "com.example.blue";
-    static final String PACKAGE_YELLOW = "com.example.yellow";
-    static final String PACKAGE_ORANGE = "com.example.orange";
-
-    static final String PROCESS_SYSTEM = "system";
-
-    static final String CLASS_RED = "com.example.red.Red";
-    static final String CLASS_GREEN = "com.example.green.Green";
-    static final String CLASS_BLUE = "com.example.blue.Blue";
-    static final String CLASS_YELLOW = "com.example.yellow.Yellow";
-    static final String CLASS_ORANGE = "com.example.orange.Orange";
-
-    static int getUidForPackage(@NonNull String packageName) {
-        switch (packageName) {
-            case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
-            case PACKAGE_PHONE: return android.os.Process.PHONE_UID;
-            case PACKAGE_RED: return android.os.Process.FIRST_APPLICATION_UID + 1;
-            case PACKAGE_GREEN: return android.os.Process.FIRST_APPLICATION_UID + 2;
-            case PACKAGE_BLUE: return android.os.Process.FIRST_APPLICATION_UID + 3;
-            case PACKAGE_YELLOW: return android.os.Process.FIRST_APPLICATION_UID + 4;
-            case PACKAGE_ORANGE: return android.os.Process.FIRST_APPLICATION_UID + 5;
-            default: throw new IllegalArgumentException();
-        }
-    }
-
-    static int getUidForPackage(@NonNull String packageName, int userId) {
-        return UserHandle.getUid(userId, getUidForPackage(packageName));
-    }
-
     /**
      * Baseline verification of common debugging infrastructure, mostly to make
      * sure it doesn't crash.
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 6792cfe..3803244 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -43,6 +43,7 @@
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_MAX;
 import static android.app.NotificationManager.IMPORTANCE_NONE;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
@@ -72,6 +73,7 @@
 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
 import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
+import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 import static android.service.notification.Adjustment.KEY_IMPORTANCE;
 import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -163,6 +165,7 @@
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
 import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
 import android.companion.ICompanionDeviceManager;
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.BroadcastReceiver;
@@ -3820,6 +3823,7 @@
         when(mCompanionMgr.getAssociations(PKG, mUserId))
                 .thenReturn(singletonList(mock(AssociationInfo.class)));
         mListener = mock(ManagedServices.ManagedServiceInfo.class);
+        mListener.component = new ComponentName(PKG, PKG);
         when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
         when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
 
@@ -3870,6 +3874,7 @@
         when(mCompanionMgr.getAssociations(PKG, mUserId))
                 .thenReturn(emptyList());
         mListener = mock(ManagedServices.ManagedServiceInfo.class);
+        mListener.component = new ComponentName(PKG, PKG);
         when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
         when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
         try {
@@ -12777,6 +12782,145 @@
         verify(mSnoozeHelper).clearData(anyInt());
     }
 
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_mappedToImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_systemCaller_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        mService.isSystemUid = true;
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        when(mCompanionMgr.getAssociations(anyString(), anyInt()))
+                .thenReturn(ImmutableList.of(
+                        new AssociationInfo.Builder(1, mUserId, "package")
+                                .setDisplayName("My watch")
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .build()));
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_withoutCompat_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void getNotificationPolicy_mappedFromImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        mBinderService.getNotificationPolicy("package");
+
+        verify(zenHelper).getNotificationPolicyFromImplicitZenRule(eq("package"));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_mappedToImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(),
+                eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_systemCaller_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        mService.isSystemUid = true;
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                eq("package"), anyString(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        when(mCompanionMgr.getAssociations(anyString(), anyInt()))
+                .thenReturn(ImmutableList.of(
+                        new AssociationInfo.Builder(1, mUserId, "package")
+                                .setDisplayName("My watch")
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .build()));
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                eq("package"), anyString(), anyInt(), anyBoolean());
+    }
+
     private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName)
             throws RemoteException {
         StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, testName, mUid, 0,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index 47f15b8..1e3b728 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -19,6 +19,7 @@
 import static com.android.server.notification.SnoozeHelper.EXTRA_KEY;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -73,6 +74,14 @@
 public class SnoozeHelperTest extends UiServiceTestCase {
     private static final String TEST_CHANNEL_ID = "test_channel_id";
 
+    private static final String XML_TAG_NAME = "snoozed-notifications";
+    private static final String XML_SNOOZED_NOTIFICATION = "notification";
+    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context";
+    private static final String XML_SNOOZED_NOTIFICATION_KEY = "key";
+    private static final String XML_SNOOZED_NOTIFICATION_TIME = "time";
+    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id";
+    private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version";
+
     @Mock SnoozeHelper.Callback mCallback;
     @Mock AlarmManager mAm;
     @Mock ManagedServices.UserProfiles mUserProfiles;
@@ -316,6 +325,53 @@
     }
 
     @Test
+    public void testSnoozeLimit_maximumPersisted() throws XmlPullParserException, IOException {
+        final long snoozeTimeout = 1234;
+        final String snoozeContext = "ctx";
+        // Serialize & deserialize notifications so that only persisted lists are used
+        TypedXmlSerializer serializer = Xml.newFastSerializer();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+        serializer.startDocument(null, true);
+        serializer.startTag(null, XML_TAG_NAME);
+        // Serialize maximum number of timed + context snoozed notifications, half of each
+        for (int i = 0; i < CONCURRENT_SNOOZE_LIMIT; i++) {
+            final boolean timedNotification = i % 2 == 0;
+            if (timedNotification) {
+                serializer.startTag(null, XML_SNOOZED_NOTIFICATION);
+            } else {
+                serializer.startTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
+            }
+            serializer.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, 1);
+            serializer.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, "key" + i);
+            if (timedNotification) {
+                serializer.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME, snoozeTimeout);
+                serializer.endTag(null, XML_SNOOZED_NOTIFICATION);
+            } else {
+                serializer.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID, snoozeContext);
+                serializer.endTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
+            }
+        }
+        serializer.endTag(null, XML_TAG_NAME);
+        serializer.endDocument();
+        serializer.flush();
+
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), "utf-8");
+        mSnoozeHelper.readXml(parser, 1);
+        // Verify that we can't snooze any more notifications
+        //  and that the limit is caused by persisted notifications
+        assertThat(mSnoozeHelper.canSnooze(1)).isFalse();
+        assertThat(mSnoozeHelper.isSnoozed(UserHandle.USER_SYSTEM, "pkg", "key0")).isFalse();
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM,
+                "pkg", "key0")).isEqualTo(snoozeTimeout);
+        assertThat(
+            mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                "key1")).isEqualTo(snoozeContext);
+    }
+
+    @Test
     public void testCancelByApp() throws Exception {
         NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
         NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM);
@@ -611,6 +667,7 @@
 
     @Test
     public void repostGroupSummary_repostsSummary() throws Exception {
+        final int snoozeDuration = 1000;
         IntArray profileIds = new IntArray();
         profileIds.add(UserHandle.USER_SYSTEM);
         when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
@@ -618,10 +675,14 @@
                 "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
         NotificationRecord r2 = getNotificationRecord(
                 "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
-        mSnoozeHelper.snooze(r, 1000);
-        mSnoozeHelper.snooze(r2, 1000);
+        final long snoozeTime = System.currentTimeMillis() + snoozeDuration;
+        mSnoozeHelper.snooze(r, snoozeDuration);
+        mSnoozeHelper.snooze(r2, snoozeDuration);
         assertEquals(2, mSnoozeHelper.getSnoozed().size());
         assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was added to the persisted list
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                r.getKey())).isAtLeast(snoozeTime);
 
         mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());
 
@@ -630,6 +691,39 @@
 
         assertEquals(1, mSnoozeHelper.getSnoozed().size());
         assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was removed from the persisted list
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                r.getKey())).isEqualTo(0);
+    }
+
+    @Test
+    public void snoozeWithContext_repostGroupSummary_removesPersisted() throws Exception {
+        final String snoozeContext = "zzzzz";
+        IntArray profileIds = new IntArray();
+        profileIds.add(UserHandle.USER_SYSTEM);
+        when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
+        NotificationRecord r = getNotificationRecord(
+                "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
+        NotificationRecord r2 = getNotificationRecord(
+                "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
+        mSnoozeHelper.snooze(r, snoozeContext);
+        mSnoozeHelper.snooze(r2, snoozeContext);
+        assertEquals(2, mSnoozeHelper.getSnoozed().size());
+        assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was added to the persisted list
+        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
+            "pkg", r.getKey())).isEqualTo(snoozeContext);
+
+        mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());
+
+        verify(mCallback, times(1)).repost(UserHandle.USER_SYSTEM, r, false);
+        verify(mCallback, never()).repost(UserHandle.USER_SYSTEM, r2, false);
+
+        assertEquals(1, mSnoozeHelper.getSnoozed().size());
+        assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was removed from the persisted list
+        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
+                "pkg", r.getKey())).isNull();
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 27e8f36..8f30f41 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -54,6 +54,15 @@
         return mRankingHelper;
     }
 
+    /**
+     * Sets {@link #isSystemUid} and {@link #isSystemAppId} to {@code false}, so that calls to NMS
+     * methods don't succeed {@link #isCallingUidSystem()} and similar checks.
+     */
+    void setCallerIsNormalPackage() {
+        isSystemUid = false;
+        isSystemAppId = false;
+    }
+
     @Override
     protected boolean isCallingUidSystem() {
         countSystemChecks++;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
new file mode 100644
index 0000000..6cc1c43
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static com.android.server.notification.ZenAdapters.notificationPolicyToZenPolicy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.NotificationManager.Policy;
+import android.service.notification.ZenPolicy;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ZenAdaptersTest extends UiServiceTestCase {
+
+    @Test
+    public void notificationPolicyToZenPolicy_allCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_ANY, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_ANYONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_starredCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_STARRED,
+                0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_repeatCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_noCallers() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_conversationsAllowedSendersUnset() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CONVERSATIONS, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo(ZenPolicy.STATE_UNSET);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_conversationsNotAllowedSendersUnset() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo(
+                ZenPolicy.STATE_DISALLOW);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_setEffects() {
+        Policy policy = new Policy(0, 0, 0,
+                Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+
+        assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_ALLOW);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_unsetEffects() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_UNSET);
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index e8201fd..37aeb57 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -22,6 +22,7 @@
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ENABLED;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
+import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS;
@@ -32,6 +33,7 @@
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM;
 import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY;
+import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS;
 import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
@@ -40,8 +42,11 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
 import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_OFF;
 import static android.service.notification.Condition.STATE_FALSE;
 import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
 
 import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS;
 import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED;
@@ -50,6 +55,8 @@
 import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
 import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -72,6 +79,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -82,6 +90,7 @@
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
@@ -92,6 +101,7 @@
 import android.media.AudioSystem;
 import android.media.VolumePolicy;
 import android.net.Uri;
+import android.os.Parcel;
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -100,6 +110,7 @@
 import android.service.notification.Condition;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.service.notification.ZenModeConfig.ZenRule;
 import android.service.notification.ZenModeDiff;
 import android.service.notification.ZenPolicy;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -124,6 +135,7 @@
 import com.android.server.notification.ManagedServices.UserProfiles;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.junit.Before;
@@ -157,27 +169,29 @@
     private static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE";
     private static final String SCHEDULE_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE";
     private static final String CUSTOM_PKG_NAME = "not.android";
+    private static final String CUSTOM_APP_LABEL = "This is not Android";
     private static final int CUSTOM_PKG_UID = 1;
     private static final String CUSTOM_RULE_ID = "custom_rule";
 
-    private final String NAME = "name";
-    private final ComponentName OWNER = new ComponentName("pkg", "cls");
-    private final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
-    private final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
-    private final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
+    private static final String NAME = "name";
+    private static final ComponentName OWNER = new ComponentName("pkg", "cls");
+    private static final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
+    private static final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
+    private static final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
             .authority("authority")
             .appendPath("path")
             .appendPath("test")
             .build();
 
-    private final Condition CONDITION = new Condition(CONDITION_ID, "", Condition.STATE_TRUE);
-    private final String TRIGGER_DESC = "Every Night, 10pm to 6am";
-    private final int TYPE = TYPE_BEDTIME;
-    private final boolean ALLOW_MANUAL = true;
-    private final int ICON_RES_ID = 1234;
-    private final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
-    private final boolean ENABLED = true;
-    private final int CREATION_TIME = 123;
+    private static final Condition CONDITION = new Condition(CONDITION_ID, "",
+            Condition.STATE_TRUE);
+    private static final String TRIGGER_DESC = "Every Night, 10pm to 6am";
+    private static final int TYPE = TYPE_BEDTIME;
+    private static final boolean ALLOW_MANUAL = true;
+    private static final int ICON_RES_ID = 1234;
+    private static final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
+    private static final boolean ENABLED = true;
+    private static final int CREATION_TIME = 123;
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -227,6 +241,10 @@
                 .thenReturn(CUSTOM_PKG_UID);
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
                 new String[] {pkg});
+        ApplicationInfo mockAppInfo = mock(ApplicationInfo.class);
+        when(mockAppInfo.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL);
+        when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt()))
+                .thenReturn(mockAppInfo);
         mZenModeHelper.mPm = mPackageManager;
 
         mZenModeEventLogger.reset();
@@ -334,7 +352,7 @@
 
     @Test
     public void testZenOff_NoMuteApplied() {
-        mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
@@ -635,7 +653,7 @@
 
         // 3. apply zen off - verify zen is set to previous ringer (normal)
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
                 mZenModeHelper.TAG);
@@ -721,7 +739,7 @@
 
         // 3.  apply zen off - verify ringer remains normal
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
                 mZenModeHelper.TAG);
@@ -746,7 +764,7 @@
 
         // 3. apply zen-off - verify ringer is still silent
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT,
                 mZenModeHelper.TAG);
@@ -781,7 +799,7 @@
 
         // 4.  apply zen off - verify ringer still silenced
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT,
                 mZenModeHelper.TAG);
@@ -795,7 +813,7 @@
 
         // apply zen off multiple times - verify ringer is not set to normal
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = null; // will evaluate config to zen mode off
         for (int i = 0; i < 3; i++) {
             // if zen doesn't change, zen should not reapply itself to the ringer
@@ -809,7 +827,7 @@
     public void testSilentRingerSavedOnZenOff_startsZenOn() {
         AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class);
         mZenModeHelper.mAudioManager = mAudioManager;
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = new ZenModeConfig();
 
         // previously set silent ringer
@@ -836,7 +854,7 @@
     public void testVibrateRingerSavedOnZenOff_startsZenOn() {
         AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class);
         mZenModeHelper.mAudioManager = mAudioManager;
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = new ZenModeConfig();
 
         // previously set silent ringer
@@ -1209,7 +1227,7 @@
                 .allowMedia(false)
                 .allowRepeatCallers(false)
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)
-                .allowMessages(ZenPolicy.PEOPLE_TYPE_CONTACTS)
+                .allowMessages(PEOPLE_TYPE_CONTACTS)
                 .allowEvents(true)
                 .allowReminders(false)
                 .build();
@@ -2023,10 +2041,10 @@
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode);
 
         // and also that it works to turn it back off again
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "",
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "",
                 Process.SYSTEM_UID, true);
 
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
+        assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
     }
 
     @Test
@@ -2041,7 +2059,7 @@
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
         // "not an action by the user" because some other app is changing zen mode
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID,
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID,
                 false);
 
         // In total, this should be 2 loggable changes
@@ -2060,7 +2078,7 @@
         //   - resulting DNDPolicyProto the same as the values in setupZenConfig()
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
@@ -2080,7 +2098,7 @@
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(1));
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertFalse(mZenModeEventLogger.getIsUserAction(1));
@@ -2144,7 +2162,7 @@
         //   - zen policy is the same as the set-up zen config
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
@@ -2157,7 +2175,7 @@
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1));
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertTrue(mZenModeEventLogger.getIsUserAction(1));
@@ -2201,7 +2219,7 @@
 
         // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode
         // is off.
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "",
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "",
                 Process.SYSTEM_UID, true);
 
         // Change the policy again
@@ -2305,7 +2323,7 @@
         // what the event should reflect. At this time, the policy is the same as initial setup.
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertFalse(mZenModeEventLogger.getIsUserAction(0));
@@ -2355,7 +2373,7 @@
         mZenModeHelper.evaluateZenModeLocked("test", true);
 
         // Check that the change actually took: zen mode should be off now
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
+        assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
 
         // but still, nothing should've been logged
         assertEquals(0, mZenModeEventLogger.numLoggedChanges());
@@ -2483,7 +2501,7 @@
                 true);
 
         // Turn off manual mode, call from a package: don't reset UID even though enabler is set
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null,
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null,
                 CUSTOM_PKG_NAME, "", 12345, false);
 
         // And likewise when turning it back on again
@@ -2660,8 +2678,11 @@
     }
 
     @Test
-    public void testCreateAutomaticZenRule_allFields() {
+    public void zenRuleToAutomaticZenRule_allFields() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
+                new String[] {OWNER.getPackageName()});
+
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = CONFIG_ACTIVITY;
         rule.component = OWNER;
@@ -2682,7 +2703,8 @@
         rule.iconResId = ICON_RES_ID;
         rule.triggerDescription = TRIGGER_DESC;
 
-        AutomaticZenRule actual = mZenModeHelper.createAutomaticZenRule(rule);
+        mZenModeHelper.mConfig.automaticRules.put(rule.id, rule);
+        AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(rule.id);
 
         assertEquals(NAME, actual.getName());
         assertEquals(OWNER, actual.getOwner());
@@ -2915,8 +2937,253 @@
         assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing);
     }
 
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                null, true));
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true));
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_TRUE);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_OFF);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_FALSE);
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_OFF);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_TRUE);
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() {
+        mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        withoutWtfCrash(
+                () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME,
+                        CUSTOM_PKG_UID,
+                        ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_createsImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy);
+
+        ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
+                .disallowAllSounds()
+                .allowCalls(PEOPLE_TYPE_CONTACTS)
+                .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
+                .hideAllVisualEffects()
+                .build();
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                expectedZenPolicy, /* conditionActive= */ null));
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_updatesImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        Policy original = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                original);
+
+        // Change priorityCallSenders: contacts -> starred.
+        Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated);
+
+        ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
+                .disallowAllSounds()
+                .allowCalls(PEOPLE_TYPE_STARRED)
+                .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
+                .hideAllVisualEffects()
+                .build();
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                expectedZenPolicy, /* conditionActive= */ null));
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() {
+        mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        withoutWtfCrash(
+                () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME,
+                        CUSTOM_PKG_UID, new Policy(0, 0, 0)));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_returnsSetPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        Policy writtenPolicy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), STATE_FALSE,
+                CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                writtenPolicy);
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isEqualTo(writtenPolicy);
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+        mZenModeHelper.mConfig.allowCalls = true;
+        mZenModeHelper.mConfig.allowConversations = false;
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isNotNull();
+        assertThat(readPolicy.allowCalls()).isTrue();
+        assertThat(readPolicy.allowConversations()).isFalse();
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        mZenModeHelper.mConfig.allowCalls = true;
+        mZenModeHelper.mConfig.allowConversations = false;
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isNotNull();
+        assertThat(readPolicy.allowCalls()).isTrue();
+        assertThat(readPolicy.allowConversations()).isFalse();
+    }
+
+    private static final Correspondence<ZenRule, ZenRule> IGNORE_TIMESTAMPS =
+            Correspondence.transforming(zr -> {
+                Parcel p = Parcel.obtain();
+                try {
+                    zr.writeToParcel(p, 0);
+                    p.setDataPosition(0);
+                    ZenRule copy = new ZenRule(p);
+                    copy.creationTime = 0;
+                    return copy;
+                } finally {
+                    p.recycle();
+                }
+            },
+            "Ignoring timestamps");
+
+    private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
+            @Nullable Boolean conditionActive) {
+        ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.id = "implicit_" + ownerPkg;
+        rule.conditionId = Uri.parse("condition://android/implicit/" + ownerPkg);
+        if (conditionActive != null) {
+            rule.condition = conditionActive
+                    ? new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE)
+                    : new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_deactivated),
+                            STATE_FALSE);
+        }
+        rule.zenMode = zenMode;
+        rule.zenPolicy = policy;
+        rule.pkg = ownerPkg;
+        rule.name = CUSTOM_APP_LABEL;
+        rule.enabled = true;
+        return rule;
+    }
+
     private void setupZenConfig() {
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig.allowAlarms = false;
         mZenModeHelper.mConfig.allowMedia = false;
         mZenModeHelper.mConfig.allowSystem = false;
@@ -2965,6 +3232,15 @@
         assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber());
     }
 
+    private static void withoutWtfCrash(Runnable test) {
+        Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {});
+        try {
+            test.run();
+        } finally {
+            Log.setWtfHandler(oldHandler);
+        }
+    }
+
     /**
      * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml()
      */
diff --git a/telephony/java/android/telephony/satellite/ISatelliteCapabilitiesCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteCapabilitiesCallback.aidl
new file mode 100644
index 0000000..4c37a6d
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/ISatelliteCapabilitiesCallback.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.telephony.satellite.SatelliteCapabilities;
+
+/**
+ * Interface for satellite capabilities change callback.
+ * @hide
+ */
+oneway interface ISatelliteCapabilitiesCallback {
+    /**
+     * Called when satellite capability has changed.
+     *
+     * @param capabilities The new satellite capability.
+     */
+    void onSatelliteCapabilitiesChanged(in SatelliteCapabilities capabilities);
+}
+
diff --git a/telephony/java/android/telephony/satellite/SatelliteCapabilitiesCallback.java b/telephony/java/android/telephony/satellite/SatelliteCapabilitiesCallback.java
new file mode 100644
index 0000000..b68dd5a
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteCapabilitiesCallback.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import com.android.internal.telephony.flags.Flags;
+
+/**
+ * A callback class for satellite capabilities change events.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+public interface SatelliteCapabilitiesCallback {
+    /**
+     * Called when satellite capability has changed.
+     * @param capabilities The new satellite capabilities.
+     */
+    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    void onSatelliteCapabilitiesChanged(@NonNull SatelliteCapabilities capabilities);
+}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index f18cbea..71786b3 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -81,6 +81,9 @@
             new ConcurrentHashMap<>();
     private static final ConcurrentHashMap<NtnSignalStrengthCallback, INtnSignalStrengthCallback>
             sNtnSignalStrengthCallbackMap = new ConcurrentHashMap<>();
+    private static final ConcurrentHashMap<SatelliteCapabilitiesCallback,
+            ISatelliteCapabilitiesCallback>
+            sSatelliteCapabilitiesCallbackMap = new ConcurrentHashMap<>();
 
     private final int mSubId;
 
@@ -2018,7 +2021,7 @@
      * {@link TelephonyManager#unregisterTelephonyCallback(TelephonyCallback)}..
      * </p>
      *
-     * @param callback The callback that was passed to
+     * @param callback The callback that was passed to.
      * {@link #registerForNtnSignalStrengthChanged(Executor, NtnSignalStrengthCallback)}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
@@ -2046,9 +2049,84 @@
             loge("unregisterForNtnSignalStrengthChanged() RemoteException: " + ex);
             ex.rethrowFromSystemServer();
         }
-
     }
 
+    /**
+     * Registers for satellite capabilities change event from the satellite service.
+     *
+     * @param executor The executor on which the callback will be called.
+     * @param callback The callback to handle the satellite capabilities changed event.
+     *
+     * @throws SecurityException if the caller doesn't have required permission.
+     * @throws IllegalStateException if the Telephony process is not currently available.
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    @SatelliteResult public int registerForSatelliteCapabilitiesChanged(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull SatelliteCapabilitiesCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                ISatelliteCapabilitiesCallback internalCallback =
+                        new ISatelliteCapabilitiesCallback.Stub() {
+                            @Override
+                            public void onSatelliteCapabilitiesChanged(
+                                    SatelliteCapabilities capabilities) {
+                                executor.execute(() -> Binder.withCleanCallingIdentity(
+                                        () -> callback.onSatelliteCapabilitiesChanged(
+                                                capabilities)));
+                            }
+                        };
+                sSatelliteCapabilitiesCallbackMap.put(callback, internalCallback);
+                return telephony.registerForSatelliteCapabilitiesChanged(mSubId, internalCallback);
+            } else {
+                throw new IllegalStateException("Telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("registerForSatelliteCapabilitiesChanged() RemoteException: " + ex);
+            ex.rethrowFromSystemServer();
+        }
+        return SATELLITE_RESULT_REQUEST_FAILED;
+    }
+
+    /**
+     * Unregisters for satellite capabilities change event from the satellite service.
+     * If callback was not registered before, the request will be ignored.
+     *
+     * @param callback The callback that was passed to.
+     * {@link #registerForSatelliteCapabilitiesChanged(Executor, SatelliteCapabilitiesCallback)}.
+     *
+     * @throws SecurityException if the caller doesn't have required permission.
+     * @throws IllegalStateException if the Telephony process is not currently available.
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    public void unregisterForSatelliteCapabilitiesChanged(
+            @NonNull SatelliteCapabilitiesCallback callback) {
+        Objects.requireNonNull(callback);
+        ISatelliteCapabilitiesCallback internalCallback =
+                sSatelliteCapabilitiesCallbackMap.remove(callback);
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                if (internalCallback != null) {
+                    telephony.unregisterForSatelliteCapabilitiesChanged(mSubId, internalCallback);
+                } else {
+                    loge("unregisterForSatelliteCapabilitiesChanged: No internal callback.");
+                }
+            } else {
+                throw new IllegalStateException("Telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("unregisterForSatelliteCapabilitiesChanged() RemoteException: " + ex);
+            ex.rethrowFromSystemServer();
+        }
+    }
 
     private static ITelephony getITelephony() {
         ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
diff --git a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
index d44ddfa..ccca5ad 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
@@ -19,6 +19,7 @@
 import android.telephony.satellite.stub.NtnSignalStrength;
 import android.telephony.satellite.stub.NTRadioTechnology;
 import android.telephony.satellite.stub.PointingInfo;
+import android.telephony.satellite.stub.SatelliteCapabilities;
 import android.telephony.satellite.stub.SatelliteDatagram;
 import android.telephony.satellite.stub.SatelliteModemState;
 
@@ -62,7 +63,15 @@
 
     /**
      * Called when NTN signal strength changes.
+     *
      * @param ntnSignalStrength The new NTN signal strength.
      */
     void onNtnSignalStrengthChanged(in NtnSignalStrength ntnSignalStrength);
+
+    /**
+     * Called when satellite capabilities of the satellite service have changed.
+     *
+     * @param SatelliteCapabilities The current satellite capabilities.
+     */
+    void onSatelliteCapabilitiesChanged(in SatelliteCapabilities capabilities);
 }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index c212e35..15a20cb 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -68,6 +68,7 @@
 import android.telephony.ims.aidl.IImsRegistrationCallback;
 import android.telephony.ims.aidl.IRcsConfigCallback;
 import android.telephony.satellite.INtnSignalStrengthCallback;
+import android.telephony.satellite.ISatelliteCapabilitiesCallback;
 import android.telephony.satellite.ISatelliteDatagramCallback;
 import android.telephony.satellite.ISatelliteTransmissionUpdateCallback;
 import android.telephony.satellite.ISatelliteProvisionStateCallback;
@@ -3123,4 +3124,27 @@
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
     void unregisterForNtnSignalStrengthChanged(int subId,
             in INtnSignalStrengthCallback callback);
+
+    /**
+     * Registers for satellite capabilities change event from the satellite service.
+     *
+     * @param executor The executor on which the callback will be called.
+     * @param callback The callback to handle the satellite capabilities changed event.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+    int registerForSatelliteCapabilitiesChanged(int subId,
+            in ISatelliteCapabilitiesCallback callback);
+
+    /**
+     * Unregisters for satellite capabilities change event from the satellite service.
+     * If callback was not registered before, the request will be ignored.
+     *
+     * @param callback The callback that was passed to.
+     * {@link #registerForSatelliteCapabilitiesChanged(Executor, SatelliteCapabilitiesCallback)}.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+    void unregisterForSatelliteCapabilitiesChanged(int subId,
+            in ISatelliteCapabilitiesCallback callback);
 }