[SB][Notifs] Add PromotedNotificationsProvider to auto-promote notifs.

PromotedNotificationsProvider is an interface with a single method,
`shouldPromote(entry: NotificationEntry): Boolean` that lets us
tell which notifications should be promoted. In AOSP, it just promotes
notifications with the FLAG_PROMOTED_ONGOING flag set.

Bug: 364653005
Flag: android.app.ui_rich_ongoing
Test: atest ActiveNotificationsInteractorTest
PromotedNotificationsProviderTest

Change-Id: I1913b65c79eb5a668a2f34e4ed2706595aed97f1
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index b0bd5b7..3d65522 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -468,6 +468,15 @@
 }
 
 flag {
+    name: "status_bar_notification_chips_test"
+    namespace: "systemui"
+    description: "Flag to enable certain features that let us test the status bar notification "
+        "chips with teamfooders. This flag should *never* be released to trunkfood or nextfood."
+    bug: "361346412"
+}
+
+
+flag {
     name: "compose_bouncer"
     namespace: "systemui"
     description: "Use the new compose bouncer in SystemUI"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 6e19096..32f4164 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -66,18 +66,34 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
-            setNotifs(listOf(activeNotificationModel(key = "notif", statusBarChipIcon = null)))
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = null,
+                        isPromoted = true,
+                    )
+                )
+            )
 
             assertThat(latest).isEmpty()
         }
 
     @Test
-    fun chips_oneNotif_statusBarIconViewMatches() =
+    fun chips_onePromotedNotif_statusBarIconViewMatches() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
             val icon = mock<StatusBarIconView>()
-            setNotifs(listOf(activeNotificationModel(key = "notif", statusBarChipIcon = icon)))
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = icon,
+                        isPromoted = true,
+                    )
+                )
+            )
 
             assertThat(latest).hasSize(1)
             val chip = latest!![0]
@@ -86,7 +102,7 @@
         }
 
     @Test
-    fun chips_twoNotifs_twoChips() =
+    fun chips_onlyForPromotedNotifs() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -94,8 +110,21 @@
             val secondIcon = mock<StatusBarIconView>()
             setNotifs(
                 listOf(
-                    activeNotificationModel(key = "notif1", statusBarChipIcon = firstIcon),
-                    activeNotificationModel(key = "notif2", statusBarChipIcon = secondIcon),
+                    activeNotificationModel(
+                        key = "notif1",
+                        statusBarChipIcon = firstIcon,
+                        isPromoted = true,
+                    ),
+                    activeNotificationModel(
+                        key = "notif2",
+                        statusBarChipIcon = secondIcon,
+                        isPromoted = true,
+                    ),
+                    activeNotificationModel(
+                        key = "notif3",
+                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        isPromoted = false,
+                    ),
                 )
             )
 
@@ -118,6 +147,7 @@
                     activeNotificationModel(
                         key = "clickTest",
                         statusBarChipIcon = mock<StatusBarIconView>(),
+                        isPromoted = true,
                     )
                 )
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index b12d7c5..25d5ce5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -293,19 +293,27 @@
         }
 
     @Test
-    fun chips_singleNotifChip_primaryIsNotifSecondaryIsHidden() =
+    fun chips_singlePromotedNotif_primaryIsNotifSecondaryIsHidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
             val icon = mock<StatusBarIconView>()
-            setNotifs(listOf(activeNotificationModel(key = "notif", statusBarChipIcon = icon)))
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = icon,
+                        isPromoted = true,
+                    )
+                )
+            )
 
             assertIsNotifChip(latest!!.primary, icon)
             assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
     @Test
-    fun chips_twoNotifChips_primaryAndSecondaryAreNotifsInOrder() =
+    fun chips_twoPromotedNotifs_primaryAndSecondaryAreNotifsInOrder() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -313,8 +321,16 @@
             val secondIcon = mock<StatusBarIconView>()
             setNotifs(
                 listOf(
-                    activeNotificationModel(key = "firstNotif", statusBarChipIcon = firstIcon),
-                    activeNotificationModel(key = "secondNotif", statusBarChipIcon = secondIcon),
+                    activeNotificationModel(
+                        key = "firstNotif",
+                        statusBarChipIcon = firstIcon,
+                        isPromoted = true,
+                    ),
+                    activeNotificationModel(
+                        key = "secondNotif",
+                        statusBarChipIcon = secondIcon,
+                        isPromoted = true,
+                    ),
                 )
             )
 
@@ -323,7 +339,7 @@
         }
 
     @Test
-    fun chips_threeNotifChips_topTwoShown() =
+    fun chips_threePromotedNotifs_topTwoShown() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -332,9 +348,21 @@
             val thirdIcon = mock<StatusBarIconView>()
             setNotifs(
                 listOf(
-                    activeNotificationModel(key = "firstNotif", statusBarChipIcon = firstIcon),
-                    activeNotificationModel(key = "secondNotif", statusBarChipIcon = secondIcon),
-                    activeNotificationModel(key = "thirdNotif", statusBarChipIcon = thirdIcon),
+                    activeNotificationModel(
+                        key = "firstNotif",
+                        statusBarChipIcon = firstIcon,
+                        isPromoted = true,
+                    ),
+                    activeNotificationModel(
+                        key = "secondNotif",
+                        statusBarChipIcon = secondIcon,
+                        isPromoted = true,
+                    ),
+                    activeNotificationModel(
+                        key = "thirdNotif",
+                        statusBarChipIcon = thirdIcon,
+                        isPromoted = true,
+                    ),
                 )
             )
 
@@ -343,7 +371,7 @@
         }
 
     @Test
-    fun chips_callAndNotifs_primaryIsCallSecondaryIsNotif() =
+    fun chips_callAndPromotedNotifs_primaryIsCallSecondaryIsNotif() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -351,10 +379,15 @@
             val firstIcon = mock<StatusBarIconView>()
             setNotifs(
                 listOf(
-                    activeNotificationModel(key = "firstNotif", statusBarChipIcon = firstIcon),
+                    activeNotificationModel(
+                        key = "firstNotif",
+                        statusBarChipIcon = firstIcon,
+                        isPromoted = true,
+                    ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = mock<StatusBarIconView>(),
+                        isPromoted = true,
                     ),
                 )
             )
@@ -364,7 +397,7 @@
         }
 
     @Test
-    fun chips_screenRecordAndCallAndNotifs_notifsNotShown() =
+    fun chips_screenRecordAndCallAndPromotedNotifs_notifsNotShown() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -375,6 +408,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock<StatusBarIconView>(),
+                        isPromoted = true,
                     )
                 )
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index 9f40f60..99bda85 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -18,11 +18,14 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
@@ -44,7 +47,7 @@
     private val testScope = kosmos.testScope
     private val activeNotificationListRepository = kosmos.activeNotificationListRepository
 
-    private val underTest = kosmos.activeNotificationsInteractor
+    private val underTest by lazy { kosmos.activeNotificationsInteractor }
 
     @Test
     fun testAllNotificationsCount() =
@@ -65,14 +68,8 @@
 
             val normalNotifs =
                 listOf(
-                    activeNotificationModel(
-                        key = "notif1",
-                        callType = CallType.None,
-                    ),
-                    activeNotificationModel(
-                        key = "notif2",
-                        callType = CallType.None,
-                    )
+                    activeNotificationModel(key = "notif1", callType = CallType.None),
+                    activeNotificationModel(key = "notif2", callType = CallType.None),
                 )
 
             activeNotificationListRepository.activeNotifications.value =
@@ -129,10 +126,7 @@
             val latest by collectLastValue(underTest.ongoingCallNotification)
 
             val ongoingNotif =
-                activeNotificationModel(
-                    key = "ongoingNotif",
-                    callType = CallType.Ongoing,
-                )
+                activeNotificationModel(key = "ongoingNotif", callType = CallType.Ongoing)
 
             activeNotificationListRepository.activeNotifications.value =
                 ActiveNotificationsStore.Builder()
@@ -170,6 +164,62 @@
         }
 
     @Test
+    @DisableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun promotedOngoingNotifications_flagOff_empty() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.promotedOngoingNotifications)
+
+            val promoted1 = activeNotificationModel(key = "notif1", isPromoted = true)
+            val notPromoted2 = activeNotificationModel(key = "notif2", isPromoted = false)
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply { addIndividualNotif(promoted1) }
+                    .apply { addIndividualNotif(notPromoted2) }
+                    .build()
+
+            assertThat(latest!!).isEmpty()
+        }
+
+    @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun promotedOngoingNotifications_nonePromoted_empty() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.promotedOngoingNotifications)
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply { activeNotificationModel(key = "notif1", isPromoted = false) }
+                    .apply { activeNotificationModel(key = "notif2", isPromoted = false) }
+                    .apply { activeNotificationModel(key = "notif3", isPromoted = false) }
+                    .build()
+
+            assertThat(latest!!).isEmpty()
+        }
+
+    @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun promotedOngoingNotifications_somePromoted_hasOnlyPromoted() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.promotedOngoingNotifications)
+
+            val promoted1 = activeNotificationModel(key = "notif1", isPromoted = true)
+            val notPromoted2 = activeNotificationModel(key = "notif2", isPromoted = false)
+            val notPromoted3 = activeNotificationModel(key = "notif3", isPromoted = false)
+            val promoted4 = activeNotificationModel(key = "notif4", isPromoted = true)
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply { addIndividualNotif(promoted1) }
+                    .apply { addIndividualNotif(notPromoted2) }
+                    .apply { addIndividualNotif(notPromoted3) }
+                    .apply { addIndividualNotif(promoted4) }
+                    .build()
+
+            assertThat(latest!!).containsExactly(promoted1, promoted4)
+        }
+
+    @Test
     fun areAnyNotificationsPresent_isTrue() =
         testScope.runTest {
             val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index 572a0c1..183f901 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -16,22 +16,25 @@
 package com.android.systemui.statusbar.notification.domain.interactor
 
 import android.app.Notification
-import android.os.Bundle
+import android.app.Notification.FLAG_PROMOTED_ONGOING
+import android.platform.test.annotations.EnableFlags
 import android.service.notification.StatusBarNotification
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.RankingBuilder
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
+import com.android.systemui.statusbar.notification.promoted.promotedNotificationsProvider
 import com.android.systemui.statusbar.notification.shared.byKey
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -39,16 +42,16 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class RenderNotificationsListInteractorTest : SysuiTestCase() {
-    private val backgroundDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(backgroundDispatcher)
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
-    private val notifsRepository = ActiveNotificationListRepository()
-    private val notifsInteractor =
-        ActiveNotificationsInteractor(notifsRepository, backgroundDispatcher)
+    private val notifsRepository = kosmos.activeNotificationListRepository
+    private val notifsInteractor = kosmos.activeNotificationsInteractor
     private val underTest =
         RenderNotificationListInteractor(
             notifsRepository,
             sectionStyleProvider = mock(),
+            promotedNotificationsProvider = kosmos.promotedNotificationsProvider,
         )
 
     @Test
@@ -85,12 +88,7 @@
 
             assertThat(ranks)
                 .containsExactlyEntriesIn(
-                    mapOf(
-                        "single" to 0,
-                        "summary" to 1,
-                        "child0" to 2,
-                        "child1" to 3,
-                    )
+                    mapOf("single" to 0, "summary" to 1, "child0" to 2, "child1" to 3)
                 )
         }
 
@@ -126,6 +124,53 @@
 
             assertThat(actual).containsAtLeastEntriesIn(expected)
         }
+
+    @Test
+    @EnableFlags(PromotedNotificationUi.FLAG_NAME)
+    fun setRenderList_setsPromotionStatus() =
+        testScope.runTest {
+            val actual by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications)
+
+            val notPromoted1 = mockNotificationEntry("key1", flag = null)
+            val promoted2 = mockNotificationEntry("key2", flag = FLAG_PROMOTED_ONGOING)
+
+            underTest.setRenderedList(listOf(notPromoted1, promoted2))
+
+            assertThat(actual!!.size).isEqualTo(2)
+
+            val first = actual!![0]
+            assertThat(first.key).isEqualTo("key1")
+            assertThat(first.isPromoted).isFalse()
+
+            val second = actual!![1]
+            assertThat(second.key).isEqualTo("key2")
+            assertThat(second.isPromoted).isTrue()
+        }
+
+    private fun mockNotificationEntry(
+        key: String,
+        rank: Int = 0,
+        flag: Int? = null,
+    ): NotificationEntry {
+        val nBuilder = Notification.Builder(context, "a")
+        if (flag != null) {
+            nBuilder.setFlag(flag, true)
+        }
+        val notification = nBuilder.build()
+
+        val mockSbn =
+            mock<StatusBarNotification>() {
+                whenever(this.notification).thenReturn(notification)
+                whenever(packageName).thenReturn("com.android")
+            }
+        return mock<NotificationEntry> {
+            whenever(this.key).thenReturn(key)
+            whenever(this.icons).thenReturn(mock())
+            whenever(this.representativeEntry).thenReturn(this)
+            whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build())
+            whenever(this.sbn).thenReturn(mockSbn)
+        }
+    }
 }
 
 private fun mockGroupEntry(
@@ -139,19 +184,3 @@
         whenever(this.children).thenReturn(children)
     }
 }
-
-private fun mockNotificationEntry(key: String, rank: Int = 0): NotificationEntry {
-    val mockNotification = mock<Notification> { this.extras = Bundle() }
-    val mockSbn =
-        mock<StatusBarNotification>() {
-            whenever(notification).thenReturn(mockNotification)
-            whenever(packageName).thenReturn("com.android")
-        }
-    return mock<NotificationEntry> {
-        whenever(this.key).thenReturn(key)
-        whenever(this.icons).thenReturn(mock())
-        whenever(this.representativeEntry).thenReturn(this)
-        whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build())
-        whenever(this.sbn).thenReturn(mockSbn)
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderTest.kt
new file mode 100644
index 0000000..a9dbe63
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.promoted
+
+import android.app.Notification
+import android.app.Notification.FLAG_PROMOTED_ONGOING
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+
+@SmallTest
+class PromotedNotificationsProviderTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    private val underTest = kosmos.promotedNotificationsProvider
+
+    @Test
+    @DisableFlags(PromotedNotificationUi.FLAG_NAME)
+    fun shouldPromote_uiFlagOff_false() {
+        val entry = createNotification(FLAG_PROMOTED_ONGOING)
+
+        assertThat(underTest.shouldPromote(entry)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(PromotedNotificationUi.FLAG_NAME)
+    fun shouldPromote_uiFlagOn_notifDoesNotHaveFlag_false() {
+        val entry = createNotification(flag = null)
+
+        assertThat(underTest.shouldPromote(entry)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(PromotedNotificationUi.FLAG_NAME)
+    fun shouldPromote_uiFlagOn_notifHasFlag_true() {
+        val entry = createNotification(FLAG_PROMOTED_ONGOING)
+
+        assertThat(underTest.shouldPromote(entry)).isTrue()
+    }
+
+    private fun createNotification(flag: Int? = null): NotificationEntry {
+        val n = Notification.Builder(context, "a")
+        if (flag != null) {
+            n.setFlag(flag, true)
+        }
+
+        return NotificationEntryBuilder().setNotification(n.build()).build()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/ReferenceNotificationsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/ReferenceNotificationsModule.kt
index 925d4a5..4c25129 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/ReferenceNotificationsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/ReferenceNotificationsModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.dagger
 
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationsModule
 import com.android.systemui.statusbar.notification.row.NotificationRowModule
 import dagger.Module
 
@@ -23,5 +24,12 @@
  * A module that includes the standard notifications classes that most SysUI variants need. Variants
  * are free to not include this module and instead write a custom notifications module.
  */
-@Module(includes = [NotificationsModule::class, NotificationRowModule::class])
+@Module(
+    includes =
+        [
+            NotificationsModule::class,
+            NotificationRowModule::class,
+            PromotedNotificationsModule::class,
+        ]
+)
 object ReferenceNotificationsModule
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 697a6ce..cff5bef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -83,6 +83,9 @@
             // TODO(b/364653005): [ongoingCallNotification] should be incorporated into this flow
             // instead of being separate.
             topLevelRepresentativeNotifications
+                .map { notifs -> notifs.filter { it.isPromoted } }
+                .distinctUntilChanged()
+                .flowOn(backgroundDispatcher)
         } else {
             flowOf(emptyList())
         }
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 1008451..23da90d 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
@@ -33,6 +33,7 @@
 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationsProvider
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -50,6 +51,7 @@
 constructor(
     private val repository: ActiveNotificationListRepository,
     private val sectionStyleProvider: SectionStyleProvider,
+    private val promotedNotificationsProvider: PromotedNotificationsProvider,
 ) {
     /**
      * Sets the current list of rendered notification entries as displayed in the notification list.
@@ -57,7 +59,11 @@
     fun setRenderedList(entries: List<ListEntry>) {
         traceSection("RenderNotificationListInteractor.setRenderedList") {
             repository.activeNotifications.update { existingModels ->
-                buildActiveNotificationsStore(existingModels, sectionStyleProvider) {
+                buildActiveNotificationsStore(
+                    existingModels,
+                    sectionStyleProvider,
+                    promotedNotificationsProvider,
+                ) {
                     entries.forEach(::addListEntry)
                     setRankingsMap(entries)
                 }
@@ -69,13 +75,21 @@
 private fun buildActiveNotificationsStore(
     existingModels: ActiveNotificationsStore,
     sectionStyleProvider: SectionStyleProvider,
-    block: ActiveNotificationsStoreBuilder.() -> Unit
+    promotedNotificationsProvider: PromotedNotificationsProvider,
+    block: ActiveNotificationsStoreBuilder.() -> Unit,
 ): ActiveNotificationsStore =
-    ActiveNotificationsStoreBuilder(existingModels, sectionStyleProvider).apply(block).build()
+    ActiveNotificationsStoreBuilder(
+            existingModels,
+            sectionStyleProvider,
+            promotedNotificationsProvider,
+        )
+        .apply(block)
+        .build()
 
 private class ActiveNotificationsStoreBuilder(
     private val existingModels: ActiveNotificationsStore,
     private val sectionStyleProvider: SectionStyleProvider,
+    private val promotedNotificationsProvider: PromotedNotificationsProvider,
 ) {
     private val builder = ActiveNotificationsStore.Builder()
 
@@ -96,7 +110,7 @@
                         existingModels.createOrReuse(
                             key = entry.key,
                             summary = summaryModel,
-                            children = childModels
+                            children = childModels,
                         )
                     )
                 }
@@ -141,6 +155,7 @@
             key = key,
             groupKey = sbn.groupKey,
             whenTime = sbn.notification.`when`,
+            isPromoted = promotedNotificationsProvider.shouldPromote(this),
             isAmbient = sectionStyleProvider.isMinimized(this),
             isRowDismissed = isRowDismissed,
             isSilent = sectionStyleProvider.isSilent(this),
@@ -166,6 +181,7 @@
     key: String,
     groupKey: String?,
     whenTime: Long,
+    isPromoted: Boolean,
     isAmbient: Boolean,
     isRowDismissed: Boolean,
     isSilent: Boolean,
@@ -189,6 +205,7 @@
             key = key,
             groupKey = groupKey,
             whenTime = whenTime,
+            isPromoted = isPromoted,
             isAmbient = isAmbient,
             isRowDismissed = isRowDismissed,
             isSilent = isSilent,
@@ -212,6 +229,7 @@
             key = key,
             groupKey = groupKey,
             whenTime = whenTime,
+            isPromoted = isPromoted,
             isAmbient = isAmbient,
             isRowDismissed = isRowDismissed,
             isSilent = isSilent,
@@ -236,6 +254,7 @@
     key: String,
     groupKey: String?,
     whenTime: Long,
+    isPromoted: Boolean,
     isAmbient: Boolean,
     isRowDismissed: Boolean,
     isSilent: Boolean,
@@ -258,6 +277,7 @@
         key != this.key -> false
         groupKey != this.groupKey -> false
         whenTime != this.whenTime -> false
+        isPromoted != this.isPromoted -> false
         isAmbient != this.isAmbient -> false
         isRowDismissed != this.isRowDismissed -> false
         isSilent != this.isSilent -> false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsModule.kt
new file mode 100644
index 0000000..4be12bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsModule.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.promoted
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class PromotedNotificationsModule {
+    @Binds
+    @SysUISingleton
+    abstract fun bindPromotedNotificationsProvider(
+        impl: PromotedNotificationsProviderImpl
+    ): PromotedNotificationsProvider
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProvider.kt
new file mode 100644
index 0000000..691dc6f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProvider.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.promoted
+
+import android.app.Notification.FLAG_PROMOTED_ONGOING
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import javax.inject.Inject
+
+/** A provider for making decisions on which notifications should be promoted. */
+interface PromotedNotificationsProvider {
+    /** Returns true if the given notification should be promoted and false otherwise. */
+    fun shouldPromote(entry: NotificationEntry): Boolean
+}
+
+@SysUISingleton
+open class PromotedNotificationsProviderImpl @Inject constructor() : PromotedNotificationsProvider {
+    override fun shouldPromote(entry: NotificationEntry): Boolean {
+        if (!PromotedNotificationUi.isEnabled) {
+            return false
+        }
+        return (entry.sbn.notification.flags and FLAG_PROMOTED_ONGOING) != 0
+    }
+}
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 cf19938..19a92a2 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
@@ -36,6 +36,8 @@
     val groupKey: String?,
     /** When this notification was posted. */
     val whenTime: Long,
+    /** True if this notification should be promoted and false otherwise. */
+    val isPromoted: Boolean,
     /** Is this entry in the ambient / minimized section (lowest priority)? */
     val isAmbient: Boolean,
     /**
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
index 76bdc0d..32c582f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -28,6 +28,7 @@
     key: String,
     groupKey: String? = null,
     whenTime: Long = 0L,
+    isPromoted: Boolean = false,
     isAmbient: Boolean = false,
     isRowDismissed: Boolean = false,
     isSilent: Boolean = false,
@@ -50,6 +51,7 @@
         key = key,
         groupKey = groupKey,
         whenTime = whenTime,
+        isPromoted = isPromoted,
         isAmbient = isAmbient,
         isRowDismissed = isRowDismissed,
         isSilent = isSilent,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt
index f7acae9..067193f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt
@@ -19,8 +19,13 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.statusbar.notification.collection.provider.sectionStyleProvider
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.promoted.promotedNotificationsProvider
 
 val Kosmos.renderNotificationListInteractor by
     Kosmos.Fixture {
-        RenderNotificationListInteractor(activeNotificationListRepository, sectionStyleProvider)
+        RenderNotificationListInteractor(
+            activeNotificationListRepository,
+            sectionStyleProvider,
+            promotedNotificationsProvider,
+        )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderKosmos.kt
new file mode 100644
index 0000000..a7aa0b4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.promoted
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.promotedNotificationsProvider by Kosmos.Fixture { PromotedNotificationsProviderImpl() }