Merge "Introduce NICViewModel#iconsViewData" into main
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
index 565bf24..baa07c1 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
@@ -95,7 +95,7 @@
         tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
         initialValue
     }
-    return this.pairwiseBy(initialValueFun) { prevVal, newVal: Boolean ->
+    return this.pairwiseBy(initialValueFun) { prevVal: Boolean, newVal: Boolean ->
         if (prevVal != newVal) {
             tableLogBuffer.logChange(columnPrefix, columnName, newVal)
         }
@@ -114,7 +114,7 @@
         tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
         initialValue
     }
-    return this.pairwiseBy(initialValueFun) { prevVal, newVal: Int ->
+    return this.pairwiseBy(initialValueFun) { prevVal: Int, newVal: Int ->
         if (prevVal != newVal) {
             tableLogBuffer.logChange(columnPrefix, columnName, newVal)
         }
@@ -133,7 +133,7 @@
         tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
         initialValue
     }
-    return this.pairwiseBy(initialValueFun) { prevVal, newVal: Int? ->
+    return this.pairwiseBy(initialValueFun) { prevVal: Int?, newVal: Int? ->
         if (prevVal != newVal) {
             tableLogBuffer.logChange(columnPrefix, columnName, newVal)
         }
@@ -152,7 +152,7 @@
         tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
         initialValue
     }
-    return this.pairwiseBy(initialValueFun) { prevVal, newVal: String? ->
+    return this.pairwiseBy(initialValueFun) { prevVal: String?, newVal: String? ->
         if (prevVal != newVal) {
             tableLogBuffer.logChange(columnPrefix, columnName, newVal)
         }
@@ -176,7 +176,7 @@
         )
         initialValue
     }
-    return this.pairwiseBy(initialValueFun) { prevVal, newVal: List<T> ->
+    return this.pairwiseBy(initialValueFun) { prevVal: List<T>, newVal: List<T> ->
         if (prevVal != newVal) {
             // TODO(b/267761156): Can we log list changes without using toString?
             tableLogBuffer.logChange(columnPrefix, columnName, newVal.toString())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index ff4570e..2e1e395 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -29,8 +29,11 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
+import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.PipelineDumpable;
 import com.android.systemui.statusbar.notification.collection.PipelineDumper;
@@ -59,7 +62,9 @@
     private static final long MAX_RANKING_DELAY_MILLIS = 500L;
 
     private final Context mContext;
+    private final FeatureFlagsClassic mFeatureFlags;
     private final NotificationManager mNotificationManager;
+    private final SilentNotificationStatusIconsVisibilityInteractor mStatusIconInteractor;
     private final SystemClock mSystemClock;
     private final Executor mMainExecutor;
     private final List<NotificationHandler> mNotificationHandlers = new ArrayList<>();
@@ -75,13 +80,17 @@
     @Inject
     public NotificationListener(
             Context context,
+            FeatureFlagsClassic featureFlags,
             NotificationManager notificationManager,
+            SilentNotificationStatusIconsVisibilityInteractor statusIconInteractor,
             SystemClock systemClock,
             @Main Executor mainExecutor,
             PluginManager pluginManager) {
         super(pluginManager);
         mContext = context;
+        mFeatureFlags = featureFlags;
         mNotificationManager = notificationManager;
+        mStatusIconInteractor = statusIconInteractor;
         mSystemClock = systemClock;
         mMainExecutor = mainExecutor;
     }
@@ -95,6 +104,7 @@
     }
 
     /** Registers a listener that's notified when any notification-related settings change. */
+    @Deprecated
     public void addNotificationSettingsListener(NotificationSettingsListener listener) {
         mSettingsListeners.add(listener);
     }
@@ -230,8 +240,12 @@
 
     @Override
     public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) {
-        for (NotificationSettingsListener listener : mSettingsListeners) {
-            listener.onStatusBarIconsBehaviorChanged(hideSilentStatusIcons);
+        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            mStatusIconInteractor.setHideSilentStatusIcons(hideSilentStatusIcons);
+        } else {
+            for (NotificationSettingsListener listener : mSettingsListeners) {
+                listener.onStatusBarIconsBehaviorChanged(hideSilentStatusIcons);
+            }
         }
     }
 
@@ -294,6 +308,7 @@
         return ranking;
     }
 
+    @Deprecated
     public interface NotificationSettingsListener {
 
         default void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) { }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/NotificationListenerSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/NotificationListenerSettingsRepository.kt
new file mode 100644
index 0000000..2c706a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/NotificationListenerSettingsRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.NotificationListener
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Exposes state pertaining to settings tracked over the [NotificationListener] boundary. */
+@SysUISingleton
+class NotificationListenerSettingsRepository @Inject constructor() {
+    /** Should icons for silent notifications be shown in the status bar? */
+    val showSilentStatusIcons = MutableStateFlow(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/SilentNotificationStatusIconsVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/SilentNotificationStatusIconsVisibilityInteractor.kt
new file mode 100644
index 0000000..1248b1c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/SilentNotificationStatusIconsVisibilityInteractor.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.domain.interactor
+
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import javax.inject.Inject
+
+class SilentNotificationStatusIconsVisibilityInteractor
+@Inject
+constructor(private val repository: NotificationListenerSettingsRepository) {
+    /** Set whether icons for silent notifications be hidden in the status bar. */
+    fun setHideSilentStatusIcons(hideIcons: Boolean) {
+        repository.showSilentStatusIcons.value = !hideIcons
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index e763797..7b3a93a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -225,7 +225,7 @@
 
     /** @see NotifPipeline#getEntry(String) () */
     @Nullable
-    NotificationEntry getEntry(@NonNull String key) {
+    public NotificationEntry getEntry(@NonNull String key) {
         return mNotificationSet.get(key);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index c2a021d..07e84bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -52,9 +52,10 @@
     fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
         traceSection("StackCoordinator.onAfterRenderList") {
             controller.setNotifStats(calculateNotifStats(entries))
-            notificationIconAreaController.updateNotificationIcons(entries)
             if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
                 renderListInteractor.setRenderedList(entries)
+            } else {
+                notificationIconAreaController.updateNotificationIcons(entries)
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index c5396dd..604ecbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -15,12 +15,16 @@
  */
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.graphics.drawable.Icon
 import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.update
 
+private typealias ModelStore = Map<String, ActiveNotificationModel>
+
 /**
  * Logic for passing information from the
  * [com.android.systemui.statusbar.notification.collection.NotifPipeline] to the presentation
@@ -30,6 +34,7 @@
 @Inject
 constructor(
     private val repository: ActiveNotificationListRepository,
+    private val sectionStyleProvider: SectionStyleProvider,
 ) {
     /**
      * Sets the current list of rendered notification entries as displayed in the notification
@@ -38,21 +43,100 @@
      * @see com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository.activeNotifications
      */
     fun setRenderedList(entries: List<ListEntry>) {
-        repository.activeNotifications.update { modelsByKey ->
+        repository.activeNotifications.update { existingModels ->
             entries.associateBy(
                 keySelector = { it.key },
-                valueTransform = { it.toModel(modelsByKey[it.key]) }
+                valueTransform = { it.toModel(existingModels) },
             )
         }
     }
 
-    private fun ListEntry.toModel(existing: ActiveNotificationModel?): ActiveNotificationModel {
-        val isCurrent =
-            when {
-                existing == null -> false
-                key == existing.key -> true
-                else -> false
-            }
-        return if (isCurrent) existing!! else ActiveNotificationModel(key = key)
+    private fun ListEntry.toModel(
+        existingModels: ModelStore,
+    ): ActiveNotificationModel =
+        existingModels.createOrReuse(
+            key = key,
+            groupKey = representativeEntry?.sbn?.groupKey,
+            isAmbient = sectionStyleProvider.isMinimized(this),
+            isRowDismissed = representativeEntry?.isRowDismissed == true,
+            isSilent = sectionStyleProvider.isSilent(this),
+            isLastMessageFromReply = representativeEntry?.isLastMessageFromReply == true,
+            isSuppressedFromStatusBar = representativeEntry?.shouldSuppressStatusBar() == true,
+            isPulsing = representativeEntry?.showingPulsing() == true,
+            aodIcon = representativeEntry?.icons?.aodIcon?.sourceIcon,
+            shelfIcon = representativeEntry?.icons?.shelfIcon?.sourceIcon,
+            statusBarIcon = representativeEntry?.icons?.statusBarIcon?.sourceIcon,
+        )
+
+    private fun ModelStore.createOrReuse(
+        key: String,
+        groupKey: String?,
+        isAmbient: Boolean,
+        isRowDismissed: Boolean,
+        isSilent: Boolean,
+        isLastMessageFromReply: Boolean,
+        isSuppressedFromStatusBar: Boolean,
+        isPulsing: Boolean,
+        aodIcon: Icon?,
+        shelfIcon: Icon?,
+        statusBarIcon: Icon?
+    ): ActiveNotificationModel {
+        return this[key]?.takeIf {
+            it.isCurrent(
+                key = key,
+                groupKey = groupKey,
+                isAmbient = isAmbient,
+                isRowDismissed = isRowDismissed,
+                isSilent = isSilent,
+                isLastMessageFromReply = isLastMessageFromReply,
+                isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+                isPulsing = isPulsing,
+                aodIcon = aodIcon,
+                shelfIcon = shelfIcon,
+                statusBarIcon = statusBarIcon
+            )
+        }
+            ?: ActiveNotificationModel(
+                key = key,
+                groupKey = groupKey,
+                isAmbient = isAmbient,
+                isRowDismissed = isRowDismissed,
+                isSilent = isSilent,
+                isLastMessageFromReply = isLastMessageFromReply,
+                isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+                isPulsing = isPulsing,
+                aodIcon = aodIcon,
+                shelfIcon = shelfIcon,
+                statusBarIcon = statusBarIcon,
+            )
+    }
+
+    private fun ActiveNotificationModel.isCurrent(
+        key: String,
+        groupKey: String?,
+        isAmbient: Boolean,
+        isRowDismissed: Boolean,
+        isSilent: Boolean,
+        isLastMessageFromReply: Boolean,
+        isSuppressedFromStatusBar: Boolean,
+        isPulsing: Boolean,
+        aodIcon: Icon?,
+        shelfIcon: Icon?,
+        statusBarIcon: Icon?
+    ): Boolean {
+        return when {
+            key != this.key -> false
+            groupKey != this.groupKey -> false
+            isAmbient != this.isAmbient -> false
+            isRowDismissed != this.isRowDismissed -> false
+            isSilent != this.isSilent -> false
+            isLastMessageFromReply != this.isLastMessageFromReply -> false
+            isSuppressedFromStatusBar != this.isSuppressedFromStatusBar -> false
+            isPulsing != this.isPulsing -> false
+            aodIcon != this.aodIcon -> false
+            shelfIcon != this.shelfIcon -> false
+            statusBarIcon != this.statusBarIcon -> false
+            else -> true
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index f3e122c..1f7ab96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -32,6 +32,7 @@
     val hasFilteredOutSeenNotifications: StateFlow<Boolean> =
         notificationListRepository.hasFilteredOutSeenNotifications
 
+    /** Set whether already-seen notifications are currently filtered out of the shade. */
     fun setHasFilteredOutSeenNotifications(value: Boolean) {
         notificationListRepository.hasFilteredOutSeenNotifications.value = value
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
new file mode 100644
index 0000000..00d873e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ *
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.icon.domain.interactor
+
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import javax.inject.Inject
+import kotlin.jvm.optionals.getOrNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+
+/** Domain logic related to notification icons. */
+class NotificationIconsInteractor
+@Inject
+constructor(
+    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
+    private val bubbles: Optional<Bubbles>,
+    private val keyguardViewStateRepository: NotificationsKeyguardViewStateRepository,
+) {
+    /** Returns a subset of all active notifications based on the supplied filtration parameters. */
+    fun filteredNotifSet(
+        showAmbient: Boolean = true,
+        showLowPriority: Boolean = true,
+        showDismissed: Boolean = true,
+        showRepliedMessages: Boolean = true,
+        showPulsing: Boolean = true,
+    ): Flow<Set<ActiveNotificationModel>> {
+        return combine(
+            activeNotificationsInteractor.notifications,
+            keyguardViewStateRepository.areNotificationsFullyHidden,
+        ) { notifications, notifsFullyHidden ->
+            notifications
+                .asSequence()
+                .filter { model: ActiveNotificationModel ->
+                    shouldShowNotificationIcon(
+                        model = model,
+                        showAmbient = showAmbient,
+                        showLowPriority = showLowPriority,
+                        showDismissed = showDismissed,
+                        showRepliedMessages = showRepliedMessages,
+                        showPulsing = showPulsing,
+                        notifsFullyHidden = notifsFullyHidden,
+                    )
+                }
+                .toSet()
+        }
+    }
+
+    private fun shouldShowNotificationIcon(
+        model: ActiveNotificationModel,
+        showAmbient: Boolean,
+        showLowPriority: Boolean,
+        showDismissed: Boolean,
+        showRepliedMessages: Boolean,
+        showPulsing: Boolean,
+        notifsFullyHidden: Boolean,
+    ): Boolean {
+        return when {
+            !showAmbient && model.isAmbient -> false
+            !showLowPriority && model.isSilent -> false
+            !showDismissed && model.isRowDismissed -> false
+            !showRepliedMessages && model.isLastMessageFromReply -> false
+            !showAmbient && model.isSuppressedFromStatusBar -> false
+            !showPulsing && model.isPulsing && !notifsFullyHidden -> false
+            bubbles.getOrNull()?.isBubbleExpanded(model.key) == true -> false
+            else -> true
+        }
+    }
+}
+
+/** Domain logic related to notification icons shown on the always-on display. */
+class AlwaysOnDisplayNotificationIconsInteractor
+@Inject
+constructor(
+    deviceEntryInteractor: DeviceEntryInteractor,
+    iconsInteractor: NotificationIconsInteractor,
+) {
+    val aodNotifs: Flow<Set<ActiveNotificationModel>> =
+        deviceEntryInteractor.isBypassEnabled.flatMapLatest { isBypassEnabled ->
+            iconsInteractor.filteredNotifSet(
+                showAmbient = false,
+                showDismissed = false,
+                showRepliedMessages = false,
+                showPulsing = !isBypassEnabled,
+            )
+        }
+}
+
+/** Domain logic related to notification icons shown in the status bar. */
+class StatusBarNotificationIconsInteractor
+@Inject
+constructor(
+    iconsInteractor: NotificationIconsInteractor,
+    settingsRepository: NotificationListenerSettingsRepository,
+) {
+    val statusBarNotifs: Flow<Set<ActiveNotificationModel>> =
+        settingsRepository.showSilentStatusIcons.flatMapLatest { showSilentIcons ->
+            iconsInteractor.filteredNotifSet(
+                showAmbient = false,
+                showLowPriority = showSilentIcons,
+                showDismissed = false,
+                showRepliedMessages = false,
+            )
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
index de011db..d8a5f01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
@@ -18,13 +18,8 @@
 import android.content.Context
 import android.graphics.Rect
 import android.os.Bundle
-import android.os.Trace
 import android.view.LayoutInflater
 import android.view.View
-import android.widget.FrameLayout
-import androidx.annotation.VisibleForTesting
-import androidx.collection.ArrayMap
-import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.demomode.DemoMode
@@ -33,27 +28,18 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.RefactorFlag
 import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationListener
-import com.android.systemui.statusbar.NotificationMediaManager
 import com.android.systemui.statusbar.NotificationShelfController
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.collection.ListEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
 import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.window.StatusBarWindowController
-import com.android.wm.shell.bubbles.Bubbles
-import java.util.Optional
-import java.util.function.Function
+import com.android.systemui.statusbar.policy.ConfigurationController
 import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 
@@ -68,58 +54,34 @@
 class NotificationIconAreaControllerViewBinderWrapperImpl
 @Inject
 constructor(
-    private val context: Context,
+    context: Context,
     private val configuration: ConfigurationState,
-    private val wakeUpCoordinator: NotificationWakeUpCoordinator,
-    private val bypassController: KeyguardBypassController,
-    private val mediaManager: NotificationMediaManager,
-    notificationListener: NotificationListener,
+    private val configurationController: ConfigurationController,
     private val dozeParameters: DozeParameters,
-    private val sectionStyleProvider: SectionStyleProvider,
-    private val bubblesOptional: Optional<Bubbles>,
     demoModeController: DemoModeController,
     private val featureFlags: FeatureFlagsClassic,
-    private val statusBarWindowController: StatusBarWindowController,
     private val screenOffAnimationController: ScreenOffAnimationController,
+    private val shelfIconViewStore: ShelfNotificationIconViewStore,
     private val shelfIconsViewModel: NotificationIconContainerShelfViewModel,
-    private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel,
+    private val aodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
     private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
-) : NotificationIconAreaController, NotificationWakeUpCoordinator.WakeUpListener, DemoMode {
+    private val statusBarIconViewStore: StatusBarNotificationIconViewStore,
+    private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel,
+) : NotificationIconAreaController, DemoMode {
 
-    private val updateStatusBarIcons = Runnable { updateStatusBarIcons() }
     private val shelfRefactor = RefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR)
 
-    private var iconSize = 0
-    private var iconHPadding = 0
-    private var notificationEntries = listOf<ListEntry>()
     private var notificationIconArea: View? = null
     private var notificationIcons: NotificationIconContainer? = null
     private var shelfIcons: NotificationIconContainer? = null
     private var aodIcons: NotificationIconContainer? = null
     private var aodBindJob: DisposableHandle? = null
-    private var showLowPriority = true
-
-    @VisibleForTesting
-    val settingsListener: NotificationListener.NotificationSettingsListener =
-        object : NotificationListener.NotificationSettingsListener {
-            override fun onStatusBarIconsBehaviorChanged(hideSilentStatusIcons: Boolean) {
-                showLowPriority = !hideSilentStatusIcons
-                updateStatusBarIcons()
-            }
-        }
 
     init {
-        wakeUpCoordinator.addListener(this)
         demoModeController.addCallback(this)
-        notificationListener.addNotificationSettingsListener(settingsListener)
         initializeNotificationAreaViews(context)
     }
 
-    @VisibleForTesting
-    fun shouldShowLowPriorityIcons(): Boolean {
-        return showLowPriority
-    }
-
     /** Called by the Keyguard*ViewController whose view contains the aod icons. */
     override fun setupAodIcons(aodIcons: NotificationIconContainer) {
         val changed = this.aodIcons != null && aodIcons !== this.aodIcons
@@ -135,14 +97,12 @@
                 aodIcons,
                 aodIconsViewModel,
                 configuration,
+                configurationController,
                 dozeParameters,
                 featureFlags,
                 screenOffAnimationController,
+                aodIconViewStore,
             )
-        if (changed) {
-            updateAodNotificationIcons()
-        }
-        updateIconLayoutParams(context)
     }
 
     override fun setupShelf(notificationShelfController: NotificationShelfController) =
@@ -154,17 +114,17 @@
                 icons,
                 shelfIconsViewModel,
                 configuration,
+                configurationController,
                 dozeParameters,
                 featureFlags,
                 screenOffAnimationController,
+                shelfIconViewStore,
             )
             shelfIcons = icons
         }
     }
 
-    override fun onDensityOrFontScaleChanged(context: Context) {
-        updateIconLayoutParams(context)
-    }
+    override fun onDensityOrFontScaleChanged(context: Context) = unsupported
 
     /** Returns the view that represents the notification area. */
     override fun getNotificationInnerAreaView(): View? {
@@ -172,39 +132,9 @@
     }
 
     /** Updates the notifications with the given list of notifications to display. */
-    override fun updateNotificationIcons(entries: List<ListEntry>) {
-        notificationEntries = entries
-        updateNotificationIcons()
-    }
+    override fun updateNotificationIcons(entries: List<ListEntry>) = unsupported
 
-    private fun updateStatusBarIcons() {
-        updateIconsForLayout(
-            { entry: NotificationEntry -> entry.icons.statusBarIcon },
-            notificationIcons,
-            showAmbient = false /* showAmbient */,
-            showLowPriority = showLowPriority,
-            hideDismissed = true /* hideDismissed */,
-            hideRepliedMessages = true /* hideRepliedMessages */,
-            hideCurrentMedia = false /* hideCurrentMedia */,
-            hidePulsing = false /* hidePulsing */
-        )
-    }
-
-    override fun updateAodNotificationIcons() {
-        if (aodIcons == null) {
-            return
-        }
-        updateIconsForLayout(
-            { entry: NotificationEntry -> entry.icons.aodIcon },
-            aodIcons,
-            showAmbient = false /* showAmbient */,
-            showLowPriority = true /* showLowPriority */,
-            hideDismissed = true /* hideDismissed */,
-            hideRepliedMessages = true /* hideRepliedMessages */,
-            hideCurrentMedia = true /* hideCurrentMedia */,
-            hidePulsing = bypassController.bypassEnabled /* hidePulsing */
-        )
-    }
+    override fun updateAodNotificationIcons() = unsupported
 
     override fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) {
         notificationIcons!!.showIconIsolated(icon, animated)
@@ -222,10 +152,6 @@
         return if (aodIcons == null) 0 else aodIcons!!.height
     }
 
-    override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
-        updateAodNotificationIcons()
-    }
-
     override fun demoCommands(): List<String> {
         val commands = ArrayList<String>()
         commands.add(DemoMode.COMMAND_NOTIFICATIONS)
@@ -252,7 +178,6 @@
 
     /** Initializes the views that will represent the notification area. */
     private fun initializeNotificationAreaViews(context: Context) {
-        reloadDimens(context)
         val layoutInflater = LayoutInflater.from(context)
         notificationIconArea = inflateIconArea(layoutInflater)
         notificationIcons = notificationIconArea?.findViewById(R.id.notificationIcons)
@@ -260,233 +185,14 @@
             notificationIcons!!,
             statusBarIconsViewModel,
             configuration,
+            configurationController,
             dozeParameters,
             featureFlags,
             screenOffAnimationController,
+            statusBarIconViewStore,
         )
     }
 
-    private fun updateIconLayoutParams(context: Context) {
-        reloadDimens(context)
-        val params = generateIconLayoutParams()
-        for (i in 0 until notificationIcons!!.childCount) {
-            val child = notificationIcons!!.getChildAt(i)
-            child.layoutParams = params
-        }
-        if (shelfIcons != null) {
-            for (i in 0 until shelfIcons!!.childCount) {
-                val child = shelfIcons!!.getChildAt(i)
-                child.layoutParams = params
-            }
-        }
-        if (aodIcons != null) {
-            for (i in 0 until aodIcons!!.childCount) {
-                val child = aodIcons!!.getChildAt(i)
-                child.layoutParams = params
-            }
-        }
-    }
-
-    private fun generateIconLayoutParams(): FrameLayout.LayoutParams {
-        return FrameLayout.LayoutParams(
-            iconSize + 2 * iconHPadding,
-            statusBarWindowController.statusBarHeight
-        )
-    }
-
-    private fun reloadDimens(context: Context) {
-        val res = context.resources
-        iconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size_sp)
-        iconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
-    }
-
-    private fun shouldShowNotificationIcon(
-        entry: NotificationEntry,
-        showAmbient: Boolean,
-        showLowPriority: Boolean,
-        hideDismissed: Boolean,
-        hideRepliedMessages: Boolean,
-        hideCurrentMedia: Boolean,
-        hidePulsing: Boolean
-    ): Boolean {
-        if (!showAmbient && sectionStyleProvider.isMinimized(entry)) {
-            return false
-        }
-        if (hideCurrentMedia && entry.key == mediaManager.mediaNotificationKey) {
-            return false
-        }
-        if (!showLowPriority && sectionStyleProvider.isSilent(entry)) {
-            return false
-        }
-        if (entry.isRowDismissed && hideDismissed) {
-            return false
-        }
-        if (hideRepliedMessages && entry.isLastMessageFromReply) {
-            return false
-        }
-        // showAmbient == show in shade but not shelf
-        if (!showAmbient && entry.shouldSuppressStatusBar()) {
-            return false
-        }
-        if (
-            hidePulsing &&
-                entry.showingPulsing() &&
-                (!wakeUpCoordinator.notificationsFullyHidden || !entry.isPulseSuppressed)
-        ) {
-            return false
-        }
-        return if (bubblesOptional.isPresent && bubblesOptional.get().isBubbleExpanded(entry.key)) {
-            false
-        } else true
-    }
-
-    private fun updateNotificationIcons() {
-        Trace.beginSection("NotificationIconAreaController.updateNotificationIcons")
-        updateStatusBarIcons()
-        updateShelfIcons()
-        updateAodNotificationIcons()
-        Trace.endSection()
-    }
-
-    private fun updateShelfIcons() {
-        if (shelfIcons == null) {
-            return
-        }
-        updateIconsForLayout(
-            { entry: NotificationEntry -> entry.icons.shelfIcon },
-            shelfIcons,
-            showAmbient = true,
-            showLowPriority = true,
-            hideDismissed = false,
-            hideRepliedMessages = false,
-            hideCurrentMedia = false,
-            hidePulsing = false
-        )
-    }
-
-    /**
-     * Updates the notification icons for a host layout. This will ensure that the notification host
-     * layout will have the same icons like the ones in here.
-     *
-     * @param function A function to look up an icon view based on an entry
-     * @param hostLayout which layout should be updated
-     * @param showAmbient should ambient notification icons be shown
-     * @param showLowPriority should icons from silent notifications be shown
-     * @param hideDismissed should dismissed icons be hidden
-     * @param hideRepliedMessages should messages that have been replied to be hidden
-     * @param hidePulsing should pulsing notifications be hidden
-     */
-    private fun updateIconsForLayout(
-        function: Function<NotificationEntry, StatusBarIconView?>,
-        hostLayout: NotificationIconContainer?,
-        showAmbient: Boolean,
-        showLowPriority: Boolean,
-        hideDismissed: Boolean,
-        hideRepliedMessages: Boolean,
-        hideCurrentMedia: Boolean,
-        hidePulsing: Boolean,
-    ) {
-        val toShow = ArrayList<StatusBarIconView>(notificationEntries.size)
-        // Filter out ambient notifications and notification children.
-        for (i in notificationEntries.indices) {
-            val entry = notificationEntries[i].representativeEntry
-            if (entry != null && entry.row != null) {
-                if (
-                    shouldShowNotificationIcon(
-                        entry,
-                        showAmbient,
-                        showLowPriority,
-                        hideDismissed,
-                        hideRepliedMessages,
-                        hideCurrentMedia,
-                        hidePulsing
-                    )
-                ) {
-                    val iconView = function.apply(entry)
-                    if (iconView != null) {
-                        toShow.add(iconView)
-                    }
-                }
-            }
-        }
-
-        // In case we are changing the suppression of a group, the replacement shouldn't flicker
-        // and it should just be replaced instead. We therefore look for notifications that were
-        // just replaced by the child or vice-versa to suppress this.
-        val replacingIcons = ArrayMap<String, ArrayList<StatusBarIcon>>()
-        val toRemove = ArrayList<View>()
-        for (i in 0 until hostLayout!!.childCount) {
-            val child = hostLayout.getChildAt(i) as? StatusBarIconView ?: continue
-            if (!toShow.contains(child)) {
-                var iconWasReplaced = false
-                val removedGroupKey = child.notification.groupKey
-                for (j in toShow.indices) {
-                    val candidate = toShow[j]
-                    if (
-                        candidate.sourceIcon.sameAs(child.sourceIcon) &&
-                            candidate.notification.groupKey == removedGroupKey
-                    ) {
-                        if (!iconWasReplaced) {
-                            iconWasReplaced = true
-                        } else {
-                            iconWasReplaced = false
-                            break
-                        }
-                    }
-                }
-                if (iconWasReplaced) {
-                    var statusBarIcons = replacingIcons[removedGroupKey]
-                    if (statusBarIcons == null) {
-                        statusBarIcons = ArrayList()
-                        replacingIcons[removedGroupKey] = statusBarIcons
-                    }
-                    statusBarIcons.add(child.statusBarIcon)
-                }
-                toRemove.add(child)
-            }
-        }
-        // removing all duplicates
-        val duplicates = ArrayList<String?>()
-        for (key in replacingIcons.keys) {
-            val statusBarIcons = replacingIcons[key]!!
-            if (statusBarIcons.size != 1) {
-                duplicates.add(key)
-            }
-        }
-        replacingIcons.removeAll(duplicates)
-        hostLayout.setReplacingIcons(replacingIcons)
-        val toRemoveCount = toRemove.size
-        for (i in 0 until toRemoveCount) {
-            hostLayout.removeView(toRemove[i])
-        }
-        val params = generateIconLayoutParams()
-        for (i in toShow.indices) {
-            val v = toShow[i]
-            // The view might still be transiently added if it was just removed and added again
-            hostLayout.removeTransientView(v)
-            if (v.parent == null) {
-                if (hideDismissed) {
-                    v.setOnDismissListener(updateStatusBarIcons)
-                }
-                hostLayout.addView(v, i, params)
-            }
-        }
-        hostLayout.setChangingViewPositions(true)
-        // Re-sort notification icons
-        val childCount = hostLayout.childCount
-        for (i in 0 until childCount) {
-            val actual = hostLayout.getChildAt(i)
-            val expected = toShow[i]
-            if (actual === expected) {
-                continue
-            }
-            hostLayout.removeView(expected)
-            hostLayout.addView(expected, i)
-        }
-        hostLayout.setChangingViewPositions(false)
-        hostLayout.setReplacingIcons(null)
-    }
-
     companion object {
         val unsupported: Nothing
             get() =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index 079004c..c6d7e21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -17,9 +17,12 @@
 
 import android.graphics.Rect
 import android.view.View
+import android.widget.FrameLayout
+import androidx.collection.ArrayMap
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
+import com.android.internal.policy.SystemBarUtils
 import com.android.internal.util.ContrastColorUtil
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.flags.FeatureFlagsClassic
@@ -29,14 +32,26 @@
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.onConfigChanged
 import com.android.systemui.util.children
+import com.android.systemui.util.kotlin.mapValuesNotNullTo
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.kotlin.stateFlow
+import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -47,9 +62,11 @@
         view: NotificationIconContainer,
         viewModel: NotificationIconContainerViewModel,
         configuration: ConfigurationState,
+        configurationController: ConfigurationController,
         dozeParameters: DozeParameters,
         featureFlags: FeatureFlagsClassic,
         screenOffAnimationController: ScreenOffAnimationController,
+        viewStore: IconViewStore,
     ): DisposableHandle {
         val contrastColorUtil = ContrastColorUtil.getInstance(view.context)
         return view.repeatWhenAttached {
@@ -58,49 +75,137 @@
                 launch {
                     viewModel.isDozing.collect { (isDozing, animate) ->
                         val animateIfNotBlanking = animate && !dozeParameters.displayNeedsBlanking
-                        view.setDozing(isDozing, animateIfNotBlanking, /* delay= */ 0) {
-                            viewModel.completeDozeAnimation()
-                        }
+                        view.setDozing(
+                            /* dozing = */ isDozing,
+                            /* fade = */ animateIfNotBlanking,
+                            /* delay = */ 0,
+                            /* endRunnable = */ viewModel::completeDozeAnimation,
+                        )
                     }
                 }
                 // TODO(b/278765923): this should live where AOD is bound, not inside of the NIC
                 //  view-binder
                 launch {
-                    val iconAppearTranslation =
-                        configuration
-                            .getDimensionPixelSize(R.dimen.shelf_appear_translation)
-                            .stateIn(this)
                     bindVisibility(
                         viewModel,
                         view,
+                        configuration,
                         featureFlags,
                         screenOffAnimationController,
-                        iconAppearTranslation,
-                    ) {
-                        viewModel.completeVisibilityAnimation()
-                    }
+                        onAnimationEnd = viewModel::completeVisibilityAnimation,
+                    )
                 }
                 launch {
                     viewModel.iconColors
                         .mapNotNull { lookup -> lookup.iconColors(view.viewBounds) }
                         .collect { iconLookup -> applyTint(view, iconLookup, contrastColorUtil) }
                 }
+                launch {
+                    bindIconViewData(
+                        viewModel,
+                        view,
+                        configuration,
+                        configurationController,
+                        viewStore,
+                    )
+                }
             }
         }
     }
 
-    // TODO(b/305739416): Once SBIV has its own Recommended Architecture stack, this can be moved
-    //  there and cleaned up.
+    private suspend fun bindIconViewData(
+        viewModel: NotificationIconContainerViewModel,
+        view: NotificationIconContainer,
+        configuration: ConfigurationState,
+        configurationController: ConfigurationController,
+        viewStore: IconViewStore,
+    ): Unit = coroutineScope {
+        val iconSizeFlow: Flow<Int> =
+            configuration.getDimensionPixelSize(
+                com.android.internal.R.dimen.status_bar_icon_size_sp,
+            )
+        val iconHorizontalPaddingFlow: Flow<Int> =
+            configuration.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
+        val statusBarHeightFlow: StateFlow<Int> =
+            stateFlow(changedSignals = configurationController.onConfigChanged) {
+                SystemBarUtils.getStatusBarHeight(view.context)
+            }
+        val layoutParams: Flow<FrameLayout.LayoutParams> =
+            combine(iconSizeFlow, iconHorizontalPaddingFlow, statusBarHeightFlow) {
+                iconSize,
+                iconHPadding,
+                statusBarHeight,
+                ->
+                FrameLayout.LayoutParams(iconSize + 2 * iconHPadding, statusBarHeight)
+            }
+
+        launch {
+            layoutParams.collect { params: FrameLayout.LayoutParams ->
+                for (child in view.children) {
+                    child.layoutParams = params
+                }
+            }
+        }
+
+        var prevIcons = IconsViewData()
+        viewModel.iconsViewData.sample(layoutParams, ::Pair).collect {
+            (iconsData: IconsViewData, layoutParams: FrameLayout.LayoutParams),
+            ->
+            val iconsDiff = IconsViewData.computeDifference(iconsData, prevIcons)
+            prevIcons = iconsData
+
+            val replacingIcons =
+                iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, v) ->
+                    viewStore.iconView(v.notifKey)?.statusBarIcon
+                }
+            view.setReplacingIcons(replacingIcons)
+
+            val childrenByNotifKey: Map<String, StatusBarIconView> =
+                view.children.filterIsInstance<StatusBarIconView>().associateByTo(ArrayMap()) {
+                    it.notification.key
+                }
+
+            iconsDiff.removed
+                .mapNotNull { key -> childrenByNotifKey[key] }
+                .forEach { child -> view.removeView(child) }
+
+            val toAdd = iconsDiff.added.mapNotNull { viewStore.iconView(it.notifKey) }
+            for ((i, sbiv) in toAdd.withIndex()) {
+                // The view might still be transiently added if it was just removed
+                // and added again
+                view.removeTransientView(sbiv)
+                view.addView(sbiv, i, layoutParams)
+            }
+
+            view.setChangingViewPositions(true)
+            // Re-sort notification icons
+            val childCount = view.childCount
+            for (i in 0 until childCount) {
+                val actual = view.getChildAt(i)
+                val expected = viewStore.iconView(iconsData.visibleKeys[i].notifKey)!!
+                if (actual === expected) {
+                    continue
+                }
+                view.removeView(expected)
+                view.addView(expected, i)
+            }
+            view.setChangingViewPositions(false)
+
+            view.setReplacingIcons(null)
+        }
+    }
+
+    // TODO(b/305739416): Once StatusBarIconView has its own Recommended Architecture stack, this
+    //  can be moved there and cleaned up.
     private fun applyTint(
         view: NotificationIconContainer,
         iconColors: IconColors,
         contrastColorUtil: ContrastColorUtil,
     ) {
-        view.children.filterIsInstance<StatusBarIconView>().forEach { iv ->
-            if (iv.width != 0) {
-                updateTintForIcon(iv, iconColors, contrastColorUtil)
-            }
-        }
+        view.children
+            .filterIsInstance<StatusBarIconView>()
+            .filter { it.width != 0 }
+            .forEach { iv -> updateTintForIcon(iv, iconColors, contrastColorUtil) }
     }
 
     private fun updateTintForIcon(
@@ -117,11 +222,13 @@
     private suspend fun bindVisibility(
         viewModel: NotificationIconContainerViewModel,
         view: NotificationIconContainer,
+        configuration: ConfigurationState,
         featureFlags: FeatureFlagsClassic,
         screenOffAnimationController: ScreenOffAnimationController,
-        iconAppearTranslation: StateFlow<Int>,
         onAnimationEnd: () -> Unit,
-    ) {
+    ): Unit = coroutineScope {
+        val iconAppearTranslation =
+            configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
         val statusViewMigrated = featureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)
         viewModel.isVisible.collect { (isVisible, animate) ->
             view.animate().cancel()
@@ -218,4 +325,39 @@
                 /* bottom = */ top + height,
             )
         }
+
+    /** External storage for [StatusBarIconView] instances. */
+    fun interface IconViewStore {
+        fun iconView(key: String): StatusBarIconView?
+    }
+}
+
+/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
+class ShelfNotificationIconViewStore
+@Inject
+constructor(
+    private val notifCollection: NotifCollection,
+) : IconViewStore {
+    override fun iconView(key: String): StatusBarIconView? =
+        notifCollection.getEntry(key)?.icons?.shelfIcon
+}
+
+/** [IconViewStore] for the always-on display. */
+class AlwaysOnDisplayNotificationIconViewStore
+@Inject
+constructor(
+    private val notifCollection: NotifCollection,
+) : IconViewStore {
+    override fun iconView(key: String): StatusBarIconView? =
+        notifCollection.getEntry(key)?.icons?.aodIcon
+}
+
+/** [IconViewStore] for the status bar. */
+class StatusBarNotificationIconViewStore
+@Inject
+constructor(
+    private val notifCollection: NotifCollection,
+) : IconViewStore {
+    override fun iconView(key: String): StatusBarIconView? =
+        notifCollection.getEntry(key)?.icons?.statusBarIcon
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index e9de4bd..885f449e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -30,8 +30,10 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
+import com.android.systemui.statusbar.notification.icon.domain.interactor.AlwaysOnDisplayNotificationIconsInteractor
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.util.kotlin.pairwise
@@ -55,6 +57,7 @@
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val dozeParameters: DozeParameters,
     private val featureFlags: FeatureFlagsClassic,
+    iconsInteractor: AlwaysOnDisplayNotificationIconsInteractor,
     keyguardInteractor: KeyguardInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
@@ -134,14 +137,19 @@
         onVisAnimationComplete.tryEmit(Unit)
     }
 
+    override val iconsViewData: Flow<IconsViewData> =
+        iconsInteractor.aodNotifs.map { entries ->
+            IconsViewData(
+                visibleKeys = entries.mapNotNull { it.toIconInfo(it.aodIcon) },
+            )
+        }
+
     /** Is there an expanded pulse, are we animating in response? */
     private fun isPulseExpandingAnimated(): Flow<AnimatedValue<Boolean>> {
         return notificationsKeyguardInteractor.isPulseExpanding
             .pairwise(initialValue = null)
             // If pulsing changes, start animating, unless it's the first emission
-            .map { (prev, expanding) ->
-                AnimatableEvent(expanding!!, startAnimating = prev != null)
-            }
+            .map { (prev, expanding) -> AnimatableEvent(expanding, startAnimating = prev != null) }
             .toAnimatedValueFlow(completionEvents = onVisAnimationComplete)
     }
 
@@ -164,9 +172,9 @@
                         // We only want the appear animations to happen when the notifications
                         // get fully hidden, since otherwise the un-hide animation overlaps.
                         featureFlags.isEnabled(Flags.NEW_AOD_TRANSITION) -> true
-                        else -> fullyHidden!!
+                        else -> fullyHidden
                     }
-                AnimatableEvent(fullyHidden!!, animate)
+                AnimatableEvent(fullyHidden, animate)
             }
             .toAnimatedValueFlow(completionEvents = onVisAnimationComplete)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index f305155..38eae24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -15,20 +15,33 @@
  */
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
+import com.android.systemui.statusbar.notification.icon.domain.interactor.NotificationIconsInteractor
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
 import com.android.systemui.util.ui.AnimatedValue
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 
 /** View-model for the overflow row of notification icons displayed in the notification shade. */
-class NotificationIconContainerShelfViewModel @Inject constructor() :
-    NotificationIconContainerViewModel {
+class NotificationIconContainerShelfViewModel
+@Inject
+constructor(
+    interactor: NotificationIconsInteractor,
+) : NotificationIconContainerViewModel {
     override val animationsEnabled: Flow<Boolean> = flowOf(true)
     override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
     override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
     override fun completeDozeAnimation() {}
     override fun completeVisibilityAnimation() {}
     override val iconColors: Flow<ColorLookup> = emptyFlow()
+
+    override val iconsViewData: Flow<IconsViewData> =
+        interactor.filteredNotifSet().map { entries ->
+            IconsViewData(
+                visibleKeys = entries.mapNotNull { it.toIconInfo(it.shelfIcon) },
+            )
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index ee01fcc..cdbabb6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -20,20 +20,24 @@
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
 import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor
 import com.android.systemui.util.ui.AnimatedValue
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.map
 
 /** View-model for the row of notification icons displayed in the status bar, */
 class NotificationIconContainerStatusBarViewModel
 @Inject
 constructor(
     darkIconInteractor: DarkIconInteractor,
+    iconsInteractor: StatusBarNotificationIconsInteractor,
     keyguardInteractor: KeyguardInteractor,
     notificationsInteractor: ActiveNotificationsInteractor,
     shadeInteractor: ShadeInteractor,
@@ -45,6 +49,7 @@
         ) { panelTouchesEnabled, isKeyguardShowing ->
             panelTouchesEnabled && !isKeyguardShowing
         }
+
     override val iconColors: Flow<ColorLookup> =
         combine(
             darkIconInteractor.tintAreas,
@@ -60,11 +65,19 @@
                 }
             }
         }
+
     override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
     override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
     override fun completeDozeAnimation() {}
     override fun completeVisibilityAnimation() {}
 
+    override val iconsViewData: Flow<IconsViewData> =
+        iconsInteractor.statusBarNotifs.map { entries ->
+            IconsViewData(
+                visibleKeys = entries.mapNotNull { it.toIconInfo(it.statusBarIcon) },
+            )
+        }
+
     private class IconColorsImpl(
         override val tint: Int,
         private val areas: Collection<Rect>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
index c98811b..0e8dfea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
@@ -16,6 +16,11 @@
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
 import android.graphics.Rect
+import android.graphics.drawable.Icon
+import androidx.collection.ArrayMap
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconInfo
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.util.kotlin.mapValuesNotNullTo
 import com.android.systemui.util.ui.AnimatedValue
 import kotlinx.coroutines.flow.Flow
 
@@ -37,6 +42,9 @@
     /** The colors with which to display the notification icons. */
     val iconColors: Flow<ColorLookup>
 
+    /** [IconsViewData] indicating which icons to display in the view. */
+    val iconsViewData: Flow<IconsViewData>
+
     /**
      * Signal completion of the [isDozing] animation; if [isDozing]'s [AnimatedValue.isAnimating]
      * property was `true`, calling this method will update it to `false`.
@@ -69,4 +77,126 @@
          */
         fun staticDrawableColor(viewBounds: Rect, isColorized: Boolean): Int
     }
+
+    /** Encapsulates the collection of notification icons present on the device. */
+    data class IconsViewData(
+        /** Icons that are visible in the container. */
+        val visibleKeys: List<IconInfo> = emptyList(),
+        /** Keys of icons that are "behind" the overflow dot. */
+        val collapsedKeys: Set<String> = emptySet(),
+        /** Whether the overflow dot should be shown regardless if [collapsedKeys] is empty. */
+        val forceShowDot: Boolean = false,
+    ) {
+        /** The difference between two [IconsViewData]s. */
+        data class Diff(
+            /** Icons added in the newer dataset. */
+            val added: List<IconInfo> = emptyList(),
+            /** Icons removed from the older dataset. */
+            val removed: List<String> = emptyList(),
+            /**
+             * Groups whose icon was replaced with a single new notification icon. The key of the
+             * [Map] is the notification group key, and the value is the new icon.
+             *
+             * Specifically, this models a difference where the older dataset had notification
+             * groups with a single icon in the set, and the newer dataset has a single, different
+             * icon for the same group. A view binder can use this information for special
+             * animations for this specific change.
+             */
+            val groupReplacements: Map<String, IconInfo> = emptyMap(),
+        )
+
+        companion object {
+            /**
+             * Returns an [IconsViewData.Diff] calculated from a [new] and [previous][prev]
+             * [IconsViewData] state.
+             */
+            fun computeDifference(new: IconsViewData, prev: IconsViewData): Diff {
+                val added: List<IconInfo> =
+                    new.visibleKeys.filter {
+                        it.notifKey !in prev.visibleKeys.asSequence().map { it.notifKey }
+                    }
+                val removed: List<IconInfo> =
+                    prev.visibleKeys.filter {
+                        it.notifKey !in new.visibleKeys.asSequence().map { it.notifKey }
+                    }
+                val groupsToShow: Set<IconGroupInfo> =
+                    new.visibleKeys.asSequence().map { it.groupInfo }.toSet()
+                val replacements: ArrayMap<String, IconInfo> =
+                    removed
+                        .asSequence()
+                        .filter { keyToRemove -> keyToRemove.groupInfo in groupsToShow }
+                        .groupBy { it.groupInfo.groupKey }
+                        .mapValuesNotNullTo(ArrayMap()) { (_, vs) ->
+                            vs.takeIf { it.size == 1 }?.get(0)
+                        }
+                return Diff(added, removed.map { it.notifKey }, replacements)
+            }
+        }
+    }
+
+    /** An Icon, and keys for unique identification. */
+    data class IconInfo(
+        val sourceIcon: Icon,
+        val notifKey: String,
+        val groupKey: String,
+    )
+}
+
+/**
+ * Construct an [IconInfo] out of an [ActiveNotificationModel], or return `null` if one cannot be
+ * created due to missing information.
+ */
+fun ActiveNotificationModel.toIconInfo(sourceIcon: Icon?): IconInfo? {
+    return sourceIcon?.let {
+        groupKey?.let { groupKey ->
+            IconInfo(
+                sourceIcon = sourceIcon,
+                notifKey = key,
+                groupKey = groupKey,
+            )
+        }
+    }
+}
+
+private val IconInfo.groupInfo: IconGroupInfo
+    get() = IconGroupInfo(sourceIcon, groupKey)
+
+private data class IconGroupInfo(
+    val sourceIcon: Icon,
+    val groupKey: String,
+) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as IconGroupInfo
+
+        if (groupKey != other.groupKey) return false
+        return sourceIcon.sameAs(other.sourceIcon)
+    }
+
+    override fun hashCode(): Int {
+        var result = groupKey.hashCode()
+        result = 31 * result + sourceIcon.type.hashCode()
+        when (sourceIcon.type) {
+            Icon.TYPE_BITMAP,
+            Icon.TYPE_ADAPTIVE_BITMAP -> {
+                result = 31 * result + sourceIcon.bitmap.hashCode()
+            }
+            Icon.TYPE_DATA -> {
+                result = 31 * result + sourceIcon.dataLength.hashCode()
+                result = 31 * result + sourceIcon.dataOffset.hashCode()
+            }
+            Icon.TYPE_RESOURCE -> {
+                result = 31 * result + sourceIcon.resId.hashCode()
+                result = 31 * result + sourceIcon.resPackage.hashCode()
+            }
+            Icon.TYPE_URI,
+            Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
+                result = 31 * result + sourceIcon.uriString.hashCode()
+            }
+        }
+        return result
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index ea29cab..78370ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -15,8 +15,36 @@
 
 package com.android.systemui.statusbar.notification.shared
 
+import android.graphics.drawable.Icon
+
 /** Model for entries in the notification stack. */
 data class ActiveNotificationModel(
     /** Notification key associated with this entry. */
     val key: String,
+    /** Notification group key associated with this entry. */
+    val groupKey: String?,
+    /** Is this entry in the ambient / minimized section (lowest priority)? */
+    val isAmbient: Boolean,
+    /**
+     * Is this entry dismissed? This is `true` when the user has dismissed the notification in the
+     * UI, but `NotificationManager` has not yet signalled to us that it has received the dismissal.
+     */
+    val isRowDismissed: Boolean,
+    /** Is this entry in the silent section? */
+    val isSilent: Boolean,
+    /**
+     * Does this entry represent a conversation, the last message of which was from a remote input
+     * reply?
+     */
+    val isLastMessageFromReply: Boolean,
+    /** Is this entry suppressed from appearing in the status bar as an icon? */
+    val isSuppressedFromStatusBar: Boolean,
+    /** Is this entry actively pulsing on AOD or bypassed-keyguard? */
+    val isPulsing: Boolean,
+    /** Icon to display on AOD. */
+    val aodIcon: Icon?,
+    /** Icon to display in the notification shelf. */
+    val shelfIcon: Icon?,
+    /** Icon to display in the status bar. */
+    val statusBarIcon: Icon?,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index cb85966..f41b7df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -3106,7 +3106,9 @@
             }
             // TODO: Bring these out of CentralSurfaces.
             mUserInfoControllerImpl.onDensityOrFontScaleChanged();
-            mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
+            if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+                mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
+            }
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index d3d11ea..66341ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -37,6 +37,8 @@
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.DozeInteractor;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -82,6 +84,7 @@
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final DeviceProvisionedController mDeviceProvisionedController;
     private final HeadsUpManager mHeadsUpManager;
+    private final FeatureFlagsClassic mFeatureFlags;
     private final BatteryController mBatteryController;
     private final ScrimController mScrimController;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
@@ -107,6 +110,7 @@
             WakefulnessLifecycle wakefulnessLifecycle,
             SysuiStatusBarStateController statusBarStateController,
             DeviceProvisionedController deviceProvisionedController,
+            FeatureFlagsClassic featureFlags,
             HeadsUpManager headsUpManager, BatteryController batteryController,
             ScrimController scrimController,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
@@ -130,6 +134,7 @@
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
         mAssistManagerLazy = assistManagerLazy;
         mDozeScrimController = dozeScrimController;
+        mFeatureFlags = featureFlags;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mPulseExpansionHandler = pulseExpansionHandler;
         mNotificationShadeWindowController = notificationShadeWindowController;
@@ -173,8 +178,13 @@
 
     void fireNotificationPulse(NotificationEntry entry) {
         Runnable pulseSuppressedListener = () -> {
-            entry.setPulseSuppressed(true);
-            mNotificationIconAreaController.updateAodNotificationIcons();
+            if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+                mHeadsUpManager.removeNotification(
+                        entry.getKey(), /* releaseImmediately= */ true, /* animate= */ false);
+            } else {
+                entry.setPulseSuppressed(true);
+                mNotificationIconAreaController.updateAodNotificationIcons();
+            }
         };
         Assert.isMainThread();
         for (Callback callback : mCallbacks) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index f9856b0..4284c96c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -448,7 +448,7 @@
             }
         }
         replacingIcons.removeAll(duplicates);
-        hostLayout.setReplacingIcons(replacingIcons);
+        hostLayout.setReplacingIconsLegacy(replacingIcons);
 
         final int toRemoveCount = toRemove.size();
         for (int i = 0; i < toRemoveCount; i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index b15c0fd..d70edbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -40,6 +40,8 @@
 import com.android.app.animation.Interpolators;
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.settingslib.Utils;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.RefactorFlag;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.notification.stack.AnimationFilter;
@@ -48,7 +50,9 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Map;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * A container for notification icons. It handles overflowing icons properly and positions them
@@ -131,6 +135,9 @@
         }
     }.setDuration(CONTENT_FADE_DURATION);
 
+    private final RefactorFlag mIconContainerRefactorFlag =
+            RefactorFlag.forView(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
+
     /* Maximum number of icons on AOD when also showing overflow dot. */
     private int mMaxIconsOnAod;
 
@@ -156,7 +163,8 @@
     private int mIconSize;
     private boolean mDisallowNextAnimation;
     private boolean mAnimationsEnabled = true;
-    private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
+    private ArrayMap<String, StatusBarIcon> mReplacingIcons;
+    private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIconsLegacy;
     // Keep track of the last visible icon so collapsed container can report on its location
     private IconState mLastVisibleIconState;
     private IconState mFirstVisibleIconState;
@@ -339,23 +347,29 @@
     }
 
     private boolean isReplacingIcon(View child) {
-        if (mReplacingIcons == null) {
-            return false;
-        }
         if (!(child instanceof StatusBarIconView)) {
             return false;
         }
         StatusBarIconView iconView = (StatusBarIconView) child;
         Icon sourceIcon = iconView.getSourceIcon();
         String groupKey = iconView.getNotification().getGroupKey();
-        ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
-        if (statusBarIcons != null) {
-            StatusBarIcon replacedIcon = statusBarIcons.get(0);
-            if (sourceIcon.sameAs(replacedIcon.icon)) {
-                return true;
+        if (mIconContainerRefactorFlag.isEnabled()) {
+            if (mReplacingIcons == null) {
+                return false;
             }
+            StatusBarIcon replacedIcon = mReplacingIcons.get(groupKey);
+            return replacedIcon != null && sourceIcon.sameAs(replacedIcon.icon);
+        } else {
+            if (mReplacingIconsLegacy == null) {
+                return false;
+            }
+            ArrayList<StatusBarIcon> statusBarIcons = mReplacingIconsLegacy.get(groupKey);
+            if (statusBarIcons != null) {
+                StatusBarIcon replacedIcon = statusBarIcons.get(0);
+                return sourceIcon.sameAs(replacedIcon.icon);
+            }
+            return false;
         }
-        return false;
     }
 
     @Override
@@ -681,7 +695,13 @@
         mAnimationsEnabled = enabled;
     }
 
-    public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
+    public void setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
+        mIconContainerRefactorFlag.assertInLegacyMode();
+        mReplacingIconsLegacy = replacingIcons;
+    }
+
+    public void setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons) {
+        if (mIconContainerRefactorFlag.isUnexpectedlyInLegacyMode()) return;
         mReplacingIcons = replacingIcons;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
index 25d67af..0a2bbe5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
@@ -13,6 +13,7 @@
  */
 package com.android.systemui.statusbar.policy
 
+import android.content.res.Configuration
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -50,3 +51,20 @@
         addCallback(listener)
         awaitClose { removeCallback(listener) }
     }
+
+/**
+ * A [Flow] that emits whenever the configuration has changed.
+ *
+ * @see ConfigurationController.ConfigurationListener.onConfigChanged
+ */
+val ConfigurationController.onConfigChanged: Flow<Configuration>
+    get() = conflatedCallbackFlow {
+        val listener =
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration) {
+                    trySend(newConfig)
+                }
+            }
+        addCallback(listener)
+        awaitClose { removeCallback(listener) }
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index 31b90ba..8fe57e11 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -61,10 +61,10 @@
  *
  * Useful for code that needs to compare the current value to the previous value.
  */
-fun <T, R> Flow<T>.pairwiseBy(
-    initialValue: T,
-    transform: suspend (previousValue: T, newValue: T) -> R,
-): Flow<R> = onStart { emit(initialValue) }.pairwiseBy(transform)
+fun <S, T : S, R> Flow<T>.pairwiseBy(
+    initialValue: S,
+    transform: suspend (previousValue: S, newValue: T) -> R,
+): Flow<R> = pairwiseBy(getInitialValue = { initialValue }, transform)
 
 /**
  * Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
@@ -75,10 +75,16 @@
  *
  * Useful for code that needs to compare the current value to the previous value.
  */
-fun <T, R> Flow<T>.pairwiseBy(
-    getInitialValue: suspend () -> T,
-    transform: suspend (previousValue: T, newValue: T) -> R,
-): Flow<R> = onStart { emit(getInitialValue()) }.pairwiseBy(transform)
+fun <S, T : S, R> Flow<T>.pairwiseBy(
+    getInitialValue: suspend () -> S,
+    transform: suspend (previousValue: S, newValue: T) -> R,
+): Flow<R> = flow {
+    var previousValue: S = getInitialValue()
+    collect { newVal ->
+        emit(transform(previousValue, newVal))
+        previousValue = newVal
+    }
+}
 
 /**
  * Returns a new [Flow] that produces the two most recent emissions from [this]. Note that the new
@@ -86,7 +92,7 @@
  *
  * Useful for code that needs to compare the current value to the previous value.
  */
-fun <T> Flow<T>.pairwise(): Flow<WithPrev<T>> = pairwiseBy(::WithPrev)
+fun <T> Flow<T>.pairwise(): Flow<WithPrev<T, T>> = pairwiseBy(::WithPrev)
 
 /**
  * Returns a new [Flow] that produces the two most recent emissions from [this]. [initialValue] will
@@ -94,10 +100,11 @@
  *
  * Useful for code that needs to compare the current value to the previous value.
  */
-fun <T> Flow<T>.pairwise(initialValue: T): Flow<WithPrev<T>> = pairwiseBy(initialValue, ::WithPrev)
+fun <S, T : S> Flow<T>.pairwise(initialValue: S): Flow<WithPrev<S, T>> =
+    pairwiseBy(initialValue, ::WithPrev)
 
 /** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */
-data class WithPrev<T>(val previousValue: T, val newValue: T)
+data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T)
 
 /**
  * Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
@@ -265,112 +272,120 @@
  * immediately invoke [getValue] to establish its initial value.
  */
 inline fun <T> CoroutineScope.stateFlow(
-    changedSignals: Flow<Unit>,
+    changedSignals: Flow<*>,
     crossinline getValue: () -> T,
 ): StateFlow<T> =
     changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue())
 
 inline fun <T1, T2, T3, T4, T5, T6, R> combine(
-        flow: Flow<T1>,
-        flow2: Flow<T2>,
-        flow3: Flow<T3>,
-        flow4: Flow<T4>,
-        flow5: Flow<T5>,
-        flow6: Flow<T6>,
-        crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
+    flow: Flow<T1>,
+    flow2: Flow<T2>,
+    flow3: Flow<T3>,
+    flow4: Flow<T4>,
+    flow5: Flow<T5>,
+    flow6: Flow<T6>,
+    crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
 ): Flow<R> {
-    return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) {
-        args: Array<*> ->
+    return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*>
+        ->
         @Suppress("UNCHECKED_CAST")
         transform(
-                args[0] as T1,
-                args[1] as T2,
-                args[2] as T3,
-                args[3] as T4,
-                args[4] as T5,
-                args[5] as T6
+            args[0] as T1,
+            args[1] as T2,
+            args[2] as T3,
+            args[3] as T4,
+            args[4] as T5,
+            args[5] as T6
         )
     }
 }
 
 inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
-        flow: Flow<T1>,
-        flow2: Flow<T2>,
-        flow3: Flow<T3>,
-        flow4: Flow<T4>,
-        flow5: Flow<T5>,
-        flow6: Flow<T6>,
-        flow7: Flow<T7>,
-        crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
+    flow: Flow<T1>,
+    flow2: Flow<T2>,
+    flow3: Flow<T3>,
+    flow4: Flow<T4>,
+    flow5: Flow<T5>,
+    flow6: Flow<T6>,
+    flow7: Flow<T7>,
+    crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
 ): Flow<R> {
     return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) {
         args: Array<*> ->
         @Suppress("UNCHECKED_CAST")
         transform(
-                args[0] as T1,
-                args[1] as T2,
-                args[2] as T3,
-                args[3] as T4,
-                args[4] as T5,
-                args[5] as T6,
-                args[6] as T7
+            args[0] as T1,
+            args[1] as T2,
+            args[2] as T3,
+            args[3] as T4,
+            args[4] as T5,
+            args[5] as T6,
+            args[6] as T7
         )
     }
 }
 
 inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
-        flow: Flow<T1>,
-        flow2: Flow<T2>,
-        flow3: Flow<T3>,
-        flow4: Flow<T4>,
-        flow5: Flow<T5>,
-        flow6: Flow<T6>,
-        flow7: Flow<T7>,
-        flow8: Flow<T8>,
-        crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
+    flow: Flow<T1>,
+    flow2: Flow<T2>,
+    flow3: Flow<T3>,
+    flow4: Flow<T4>,
+    flow5: Flow<T5>,
+    flow6: Flow<T6>,
+    flow7: Flow<T7>,
+    flow8: Flow<T8>,
+    crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
 ): Flow<R> {
     return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) {
         args: Array<*> ->
         @Suppress("UNCHECKED_CAST")
         transform(
-                args[0] as T1,
-                args[1] as T2,
-                args[2] as T3,
-                args[3] as T4,
-                args[4] as T5,
-                args[5] as T6,
-                args[6] as T7,
-                args[7] as T8
+            args[0] as T1,
+            args[1] as T2,
+            args[2] as T3,
+            args[3] as T4,
+            args[4] as T5,
+            args[5] as T6,
+            args[6] as T7,
+            args[7] as T8
         )
     }
 }
 
 inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine(
-        flow: Flow<T1>,
-        flow2: Flow<T2>,
-        flow3: Flow<T3>,
-        flow4: Flow<T4>,
-        flow5: Flow<T5>,
-        flow6: Flow<T6>,
-        flow7: Flow<T7>,
-        flow8: Flow<T8>,
-        flow9: Flow<T9>,
-        crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R
+    flow: Flow<T1>,
+    flow2: Flow<T2>,
+    flow3: Flow<T3>,
+    flow4: Flow<T4>,
+    flow5: Flow<T5>,
+    flow6: Flow<T6>,
+    flow7: Flow<T7>,
+    flow8: Flow<T8>,
+    flow9: Flow<T9>,
+    crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R
 ): Flow<R> {
     return kotlinx.coroutines.flow.combine(
-        flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9
+        flow,
+        flow2,
+        flow3,
+        flow4,
+        flow5,
+        flow6,
+        flow7,
+        flow8,
+        flow9
     ) { args: Array<*> ->
         @Suppress("UNCHECKED_CAST")
         transform(
-                args[0] as T1,
-                args[1] as T2,
-                args[2] as T3,
-                args[3] as T4,
-                args[4] as T5,
-                args[5] as T6,
-                args[6] as T7,
-                args[6] as T8,
-                args[6] as T9,
+            args[0] as T1,
+            args[1] as T2,
+            args[2] as T3,
+            args[3] as T4,
+            args[4] as T5,
+            args[5] as T6,
+            args[6] as T7,
+            args[6] as T8,
+            args[6] as T9,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt
new file mode 100644
index 0000000..41cd95b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.util.kotlin
+
+/** Like [mapValues], but discards `null` values returned from [block]. */
+fun <K, V, R> Map<K, V>.mapValuesNotNull(block: (Map.Entry<K, V>) -> R?): Map<K, R> = buildMap {
+    this@mapValuesNotNull.mapValuesNotNullTo(this, block)
+}
+
+/** Like [mapValuesTo], but discards `null` values returned from [block]. */
+fun <K, V, R, M : MutableMap<in K, in R>> Map<out K, V>.mapValuesNotNullTo(
+    destination: M,
+    block: (Map.Entry<K, V>) -> R?,
+): M {
+    for (entry in this) {
+        block(entry)?.also { destination.put(entry.key, it) }
+    }
+    return destination
+}
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
index ff1d5b2..f49ba64 100644
--- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
+++ b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
@@ -75,6 +76,7 @@
     @get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(),
     @get:Provides val keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(),
     @get:Provides val mediaHierarchyManager: MediaHierarchyManager = mock(),
+    @get:Provides val notifCollection: NotifCollection = mock(),
     @get:Provides val notificationListener: NotificationListener = mock(),
     @get:Provides val notificationLockscreenUserManager: NotificationLockscreenUserManager = mock(),
     @get:Provides val notificationMediaManager: NotificationMediaManager = mock(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
index 3412679..2b3fd34 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
@@ -37,8 +37,12 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository;
+import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -68,9 +72,14 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
+        FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
+        featureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
         mListener = new NotificationListener(
                 mContext,
+                featureFlags,
                 mNotificationManager,
+                new SilentNotificationStatusIconsVisibilityInteractor(
+                        new NotificationListenerSettingsRepository()),
                 mFakeSystemClock,
                 mFakeExecutor,
                 mPluginManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index 8c5c439..ca8ea4e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -35,6 +35,7 @@
     private val underTest =
         RenderNotificationListInteractor(
             notifsRepository,
+            sectionStyleProvider = mock(),
         )
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
new file mode 100644
index 0000000..9988fc7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.notification.icon.domain.interactor
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.TestMocksModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.shared.byIsAmbient
+import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
+import com.android.systemui.statusbar.notification.shared.byIsPulsing
+import com.android.systemui.statusbar.notification.shared.byIsRowDismissed
+import com.android.systemui.statusbar.notification.shared.byIsSilent
+import com.android.systemui.statusbar.notification.shared.byIsSuppressedFromStatusBar
+import com.android.systemui.statusbar.notification.shared.byKey
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import java.util.Optional
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationIconsInteractorTest : SysuiTestCase() {
+
+    private val bubbles: Bubbles = mock()
+
+    @Component(modules = [SysUITestModule::class])
+    @SysUISingleton
+    interface TestComponent {
+        val underTest: NotificationIconsInteractor
+
+        val activeNotificationListRepository: ActiveNotificationListRepository
+        val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+        val testScope: TestScope
+
+        @Component.Factory
+        interface Factory {
+            fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent
+        }
+    }
+
+    val testComponent: TestComponent =
+        DaggerNotificationIconsInteractorTest_TestComponent.factory()
+            .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles)))
+
+    @Before
+    fun setup() =
+        with(testComponent) {
+            activeNotificationListRepository.activeNotifications.value =
+                testIcons.associateBy { it.key }
+        }
+
+    @Test
+    fun filteredEntrySet() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.filteredNotifSet())
+                assertThat(filteredSet).containsExactlyElementsIn(testIcons)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noExpandedBubbles() =
+        with(testComponent) {
+            testScope.runTest {
+                whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true)
+                val filteredSet by collectLastValue(underTest.filteredNotifSet())
+                assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1")
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noAmbient() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.filteredNotifSet(showAmbient = false))
+                assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true)
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsSuppressedFromStatusBar)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noLowPriority() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by
+                    collectLastValue(underTest.filteredNotifSet(showLowPriority = false))
+                assertThat(filteredSet).comparingElementsUsing(byIsSilent).doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noDismissed() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by
+                    collectLastValue(underTest.filteredNotifSet(showDismissed = false))
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsRowDismissed)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noRepliedMessages() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by
+                    collectLastValue(underTest.filteredNotifSet(showRepliedMessages = false))
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsLastMessageFromReply)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noPulsing_notifsNotFullyHidden() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false))
+                keyguardViewStateRepository.setNotificationsFullyHidden(false)
+                assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noPulsing_notifsFullyHidden() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false))
+                keyguardViewStateRepository.setNotificationsFullyHidden(true)
+                assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+            }
+        }
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() {
+
+    private val bubbles: Bubbles = mock()
+
+    @Component(modules = [SysUITestModule::class])
+    @SysUISingleton
+    interface TestComponent {
+        val underTest: AlwaysOnDisplayNotificationIconsInteractor
+
+        val activeNotificationListRepository: ActiveNotificationListRepository
+        val deviceEntryRepository: FakeDeviceEntryRepository
+        val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+        val testScope: TestScope
+
+        @Component.Factory
+        interface Factory {
+            fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent
+        }
+    }
+
+    val testComponent: TestComponent =
+        DaggerAlwaysOnDisplayNotificationIconsInteractorTest_TestComponent.factory()
+            .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles)))
+
+    @Before
+    fun setup() =
+        with(testComponent) {
+            activeNotificationListRepository.activeNotifications.value =
+                testIcons.associateBy { it.key }
+        }
+
+    @Test
+    fun filteredEntrySet_noExpandedBubbles() =
+        with(testComponent) {
+            testScope.runTest {
+                whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true)
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1")
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noAmbient() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true)
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsSuppressedFromStatusBar)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noDismissed() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsRowDismissed)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noRepliedMessages() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsLastMessageFromReply)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_showPulsing_notifsNotFullyHidden_bypassDisabled() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                deviceEntryRepository.setBypassEnabled(false)
+                keyguardViewStateRepository.setNotificationsFullyHidden(false)
+                assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_showPulsing_notifsFullyHidden_bypassDisabled() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                deviceEntryRepository.setBypassEnabled(false)
+                keyguardViewStateRepository.setNotificationsFullyHidden(true)
+                assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noPulsing_notifsNotFullyHidden_bypassEnabled() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                deviceEntryRepository.setBypassEnabled(true)
+                keyguardViewStateRepository.setNotificationsFullyHidden(false)
+                assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_showPulsing_notifsFullyHidden_bypassEnabled() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.aodNotifs)
+                deviceEntryRepository.setBypassEnabled(true)
+                keyguardViewStateRepository.setNotificationsFullyHidden(true)
+                assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+            }
+        }
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class StatusBarNotificationIconsInteractorTest : SysuiTestCase() {
+
+    private val bubbles: Bubbles = mock()
+
+    @Component(modules = [SysUITestModule::class])
+    @SysUISingleton
+    interface TestComponent {
+        val underTest: StatusBarNotificationIconsInteractor
+
+        val activeNotificationListRepository: ActiveNotificationListRepository
+        val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+        val notificationListenerSettingsRepository: NotificationListenerSettingsRepository
+        val testScope: TestScope
+
+        @Component.Factory
+        interface Factory {
+            fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent
+        }
+    }
+
+    val testComponent: TestComponent =
+        DaggerStatusBarNotificationIconsInteractorTest_TestComponent.factory()
+            .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles)))
+
+    @Before
+    fun setup() =
+        with(testComponent) {
+            activeNotificationListRepository.activeNotifications.value =
+                testIcons.associateBy { it.key }
+        }
+
+    @Test
+    fun filteredEntrySet_noExpandedBubbles() =
+        with(testComponent) {
+            testScope.runTest {
+                whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true)
+                val filteredSet by collectLastValue(underTest.statusBarNotifs)
+                assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1")
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noAmbient() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.statusBarNotifs)
+                assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true)
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsSuppressedFromStatusBar)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noLowPriority_whenDontShowSilentIcons() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.statusBarNotifs)
+                notificationListenerSettingsRepository.showSilentStatusIcons.value = false
+                assertThat(filteredSet).comparingElementsUsing(byIsSilent).doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_showLowPriority_whenShowSilentIcons() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.statusBarNotifs)
+                notificationListenerSettingsRepository.showSilentStatusIcons.value = true
+                assertThat(filteredSet).comparingElementsUsing(byIsSilent).contains(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noDismissed() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.statusBarNotifs)
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsRowDismissed)
+                    .doesNotContain(true)
+            }
+        }
+
+    @Test
+    fun filteredEntrySet_noRepliedMessages() =
+        with(testComponent) {
+            testScope.runTest {
+                val filteredSet by collectLastValue(underTest.statusBarNotifs)
+                assertThat(filteredSet)
+                    .comparingElementsUsing(byIsLastMessageFromReply)
+                    .doesNotContain(true)
+            }
+        }
+}
+
+private val testIcons =
+    listOf(
+        ActiveNotificationModel(
+            key = "notif1",
+        ),
+        ActiveNotificationModel(
+            key = "notif2",
+            isAmbient = true,
+        ),
+        ActiveNotificationModel(
+            key = "notif3",
+            isRowDismissed = true,
+        ),
+        ActiveNotificationModel(
+            key = "notif4",
+            isSilent = true,
+        ),
+        ActiveNotificationModel(
+            key = "notif5",
+            isLastMessageFromReply = true,
+        ),
+        ActiveNotificationModel(
+            key = "notif6",
+            isSuppressedFromStatusBar = true,
+        ),
+        ActiveNotificationModel(
+            key = "notif7",
+            isPulsing = true,
+        ),
+    )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
deleted file mode 100644
index e57986d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
+++ /dev/null
@@ -1,106 +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.statusbar.notification.icon.ui.viewbinder
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
-import com.android.systemui.flags.Flags
-import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.user.domain.UserDomainLayerModule
-import dagger.BindsInstance
-import dagger.Component
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@RunWithLooper(setAsMainLooper = true)
-class NotificationIconAreaControllerViewBinderWrapperImplTest : SysuiTestCase() {
-
-    @Mock private lateinit var dozeParams: DozeParameters
-
-    private lateinit var testComponent: TestComponent
-    private val underTest
-        get() = testComponent.underTest
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        allowTestableLooperAsMainThread()
-
-        testComponent =
-            DaggerNotificationIconAreaControllerViewBinderWrapperImplTest_TestComponent.factory()
-                .create(
-                    test = this,
-                    featureFlags =
-                        FakeFeatureFlagsClassicModule {
-                            set(Flags.FACE_AUTH_REFACTOR, value = false)
-                            set(Flags.MIGRATE_KEYGUARD_STATUS_VIEW, value = false)
-                        },
-                    mocks =
-                        TestMocksModule(
-                            dozeParameters = dozeParams,
-                        ),
-                )
-    }
-
-    @Test
-    fun testNotificationIcons_settingHideIcons() {
-        underTest.settingsListener.onStatusBarIconsBehaviorChanged(true)
-        assertFalse(underTest.shouldShowLowPriorityIcons())
-    }
-
-    @Test
-    fun testNotificationIcons_settingShowIcons() {
-        underTest.settingsListener.onStatusBarIconsBehaviorChanged(false)
-        assertTrue(underTest.shouldShowLowPriorityIcons())
-    }
-
-    @SysUISingleton
-    @Component(
-        modules =
-            [
-                SysUITestModule::class,
-                BiometricsDomainLayerModule::class,
-                UserDomainLayerModule::class,
-            ]
-    )
-    interface TestComponent {
-
-        val underTest: NotificationIconAreaControllerViewBinderWrapperImpl
-
-        @Component.Factory
-        interface Factory {
-            fun create(
-                @BindsInstance test: SysuiTestCase,
-                mocks: TestMocksModule,
-                featureFlags: FakeFeatureFlagsClassicModule,
-            ): TestComponent
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
index ed94058..0eac1ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
@@ -19,3 +19,45 @@
 
 val byKey: Correspondence<ActiveNotificationModel, String> =
     Correspondence.transforming({ it?.key }, "has a key of")
+val byIsAmbient: Correspondence<ActiveNotificationModel, Boolean> =
+    Correspondence.transforming({ it?.isAmbient }, "has an isAmbient value of")
+val byIsSuppressedFromStatusBar: Correspondence<ActiveNotificationModel, Boolean> =
+    Correspondence.transforming(
+        { it?.isSuppressedFromStatusBar },
+        "has an isSuppressedFromStatusBar value of",
+    )
+val byIsSilent: Correspondence<ActiveNotificationModel, Boolean> =
+    Correspondence.transforming({ it?.isSilent }, "has an isSilent value of")
+val byIsRowDismissed: Correspondence<ActiveNotificationModel, Boolean> =
+    Correspondence.transforming({ it?.isRowDismissed }, "has an isRowDismissed value of")
+val byIsLastMessageFromReply: Correspondence<ActiveNotificationModel, Boolean> =
+    Correspondence.transforming(
+        { it?.isLastMessageFromReply },
+        "has an isLastMessageFromReply value of"
+    )
+val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> =
+    Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of")
+
+@Suppress("TestFunctionName")
+fun ActiveNotificationModel(
+    key: String,
+    isAmbient: Boolean = false,
+    isRowDismissed: Boolean = false,
+    isSilent: Boolean = false,
+    isLastMessageFromReply: Boolean = false,
+    isSuppressedFromStatusBar: Boolean = false,
+    isPulsing: Boolean = false,
+) =
+    ActiveNotificationModel(
+        key = key,
+        groupKey = null,
+        isAmbient = isAmbient,
+        isRowDismissed = isRowDismissed,
+        isSilent = isSilent,
+        isLastMessageFromReply = isLastMessageFromReply,
+        isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+        isPulsing = isPulsing,
+        aodIcon = null,
+        shelfIcon = null,
+        statusBarIcon = null,
+    )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 593c587..472709c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -42,6 +42,8 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
+import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.DozeInteractor;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -96,18 +98,21 @@
     @Mock private BiometricUnlockController mBiometricUnlockController;
     @Mock private AuthController mAuthController;
     @Mock private DozeHost.Callback mCallback;
-
     @Mock private DozeInteractor mDozeInteractor;
+
+    private final FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mFeatureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
         mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle,
-                mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager,
-                mBatteryController, mScrimController, () -> mBiometricUnlockController,
-                () -> mAssistManager, mDozeScrimController,
-                mKeyguardUpdateMonitor, mPulseExpansionHandler,
-                mNotificationShadeWindowController, mNotificationWakeUpCoordinator,
-                mAuthController, mNotificationIconAreaController, mDozeInteractor);
+                mStatusBarStateController, mDeviceProvisionedController, mFeatureFlags,
+                mHeadsUpManager, mBatteryController, mScrimController,
+                () -> mBiometricUnlockController, () -> mAssistManager, mDozeScrimController,
+                mKeyguardUpdateMonitor, mPulseExpansionHandler, mNotificationShadeWindowController,
+                mNotificationWakeUpCoordinator, mAuthController, mNotificationIconAreaController,
+                mDozeInteractor);
 
         mDozeServiceHost.initialize(
                 mCentralSurfaces,