[SB][Call chip] Listen to ActiveNotificationsInteractor for call info.

The current OngoingCallController listens to
NotifCollectionListener#onEntryUpdated to determine what call
notifications are active and when to show the ongoing call chip. This
has two problems:

1) #onEntryUpdated does not take users into account. If you start a call
   on User 1, then switch to User 2, User 2 would still see the call
   chip even though there's no call notification shown to User 2. User 2
   has no way to stop the call either, and User 2 can't tap the chip
   because the intent isn't valid for this user.

2) For b/354930838, we want to update the call chip to use
   `Notification.smallIcon` as the icon in the chip. However,
   #onEntryUpdated is invoked before we've actually inflated the small
   icon, so the chip would show no icon (see b/355288215).

Listening to ActivteNotificationsInteractor solves both these problems:

1) The interactor automatically filters the notification list to just
   notifications for the current user.

2) The interactor only emits notifications that are currently presented
   to the user in the shade, which means we know we've already inflated
   the icon.

Bug: 328584859
Flag: com.android.systemui.status_bar_use_repos_for_call_chip

Test: Start ongoing call on one user, switch to another user -> verify
chip no longer shows up
Test: Start ongoing call in one app, then start another call in a second
app -> verify chip still shows fist app call time. End first call ->
verify chip starts showing second app call time.
Test: General smoke test of ongoing call CUJs
Test: Verify above with status_bar_screen_sharing_chips flag both
disabled and enabled

Test: atest ActiveNotificationsInteractorTest
OngoingCallControllerViaListenerTest OngoingCallControllerViaRepoTest

Change-Id: I5615cea679b4cff075390d1da4c9a918419fc76a
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 1f1495a..9df739c 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -392,6 +392,17 @@
 }
 
 flag {
+    name: "status_bar_use_repos_for_call_chip"
+    namespace: "systemui"
+    description: "Use repositories as the source of truth for call notifications shown as a chip in"
+        "the status bar"
+    bug: "328584859"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+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/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index bec8cfe..9f40f60 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
@@ -24,8 +24,11 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 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
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -56,6 +59,117 @@
         }
 
     @Test
+    fun ongoingCallNotification_noCallNotifs_null() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.ongoingCallNotification)
+
+            val normalNotifs =
+                listOf(
+                    activeNotificationModel(
+                        key = "notif1",
+                        callType = CallType.None,
+                    ),
+                    activeNotificationModel(
+                        key = "notif2",
+                        callType = CallType.None,
+                    )
+                )
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply { normalNotifs.forEach(::addIndividualNotif) }
+                    .build()
+
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun ongoingCallNotification_incomingCallNotif_null() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.ongoingCallNotification)
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply {
+                        addIndividualNotif(
+                            activeNotificationModel(
+                                key = "incomingNotif",
+                                callType = CallType.Incoming,
+                            )
+                        )
+                    }
+                    .build()
+
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun ongoingCallNotification_screeningCallNotif_null() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.ongoingCallNotification)
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply {
+                        addIndividualNotif(
+                            activeNotificationModel(
+                                key = "screeningNotif",
+                                callType = CallType.Screening,
+                            )
+                        )
+                    }
+                    .build()
+
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun ongoingCallNotification_ongoingCallNotif_hasNotif() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.ongoingCallNotification)
+
+            val ongoingNotif =
+                activeNotificationModel(
+                    key = "ongoingNotif",
+                    callType = CallType.Ongoing,
+                )
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply { addIndividualNotif(ongoingNotif) }
+                    .build()
+
+            assertThat(latest).isEqualTo(ongoingNotif)
+        }
+
+    @Test
+    fun ongoingCallNotification_multipleCallNotifs_usesEarlierNotif() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.ongoingCallNotification)
+
+            val earlierOngoingNotif =
+                activeNotificationModel(
+                    key = "earlierOngoingNotif",
+                    callType = CallType.Ongoing,
+                    whenTime = 45L,
+                )
+            val laterOngoingNotif =
+                activeNotificationModel(
+                    key = "laterOngoingNotif",
+                    callType = CallType.Ongoing,
+                    whenTime = 55L,
+                )
+
+            activeNotificationListRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply { addIndividualNotif(earlierOngoingNotif) }
+                    .apply { addIndividualNotif(laterOngoingNotif) }
+                    .build()
+
+            assertThat(latest).isEqualTo(earlierOngoingNotif)
+        }
+
+    @Test
     fun areAnyNotificationsPresent_isTrue() =
         testScope.runTest {
             val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
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 0dbc8c0..1cb59f1 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
@@ -15,11 +15,13 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.shared.CallType
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
@@ -27,6 +29,7 @@
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
+@SysUISingleton
 class ActiveNotificationsInteractor
 @Inject
 constructor(
@@ -71,6 +74,19 @@
     val allNotificationsCountValue: Int
         get() = repository.activeNotifications.value.individuals.size
 
+    /**
+     * The priority ongoing call notification, or null if there is no ongoing call.
+     *
+     * The output model is guaranteed to have [ActiveNotificationModel.callType] to be equal to
+     * [CallType.Ongoing].
+     */
+    val ongoingCallNotification: Flow<ActiveNotificationModel?> =
+        allRepresentativeNotifications.map { notifMap ->
+            // Once a call has started, its `whenTime` should stay the same, so we can use it as a
+            // stable sort value.
+            notifMap.values.filter { it.callType == CallType.Ongoing }.minByOrNull { it.whenTime }
+        }
+
     /** Are any notifications being actively presented in the notification stack? */
     val areAnyNotificationsPresent: Flow<Boolean> =
         repository.activeNotifications
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 ab54bda..5d2d56a 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,7 +15,14 @@
  */
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.app.Notification.CallStyle.CALL_TYPE_INCOMING
+import android.app.Notification.CallStyle.CALL_TYPE_ONGOING
+import android.app.Notification.CallStyle.CALL_TYPE_SCREENING
+import android.app.Notification.CallStyle.CALL_TYPE_UNKNOWN
+import android.app.Notification.EXTRA_CALL_TYPE
+import android.app.PendingIntent
 import android.graphics.drawable.Icon
+import android.service.notification.StatusBarNotification
 import android.util.ArrayMap
 import com.android.app.tracing.traceSection
 import com.android.systemui.statusbar.notification.collection.GroupEntry
@@ -27,6 +34,7 @@
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.shared.CallType
 import javax.inject.Inject
 import kotlinx.coroutines.flow.update
 
@@ -124,6 +132,7 @@
         existingModels.createOrReuse(
             key = key,
             groupKey = sbn.groupKey,
+            whenTime = sbn.notification.`when`,
             isAmbient = sectionStyleProvider.isMinimized(this),
             isRowDismissed = isRowDismissed,
             isSilent = sectionStyleProvider.isSilent(this),
@@ -135,15 +144,18 @@
             statusBarIcon = icons.statusBarIcon?.sourceIcon,
             uid = sbn.uid,
             packageName = sbn.packageName,
+            contentIntent = sbn.notification.contentIntent,
             instanceId = sbn.instanceId?.id,
             isGroupSummary = sbn.notification.isGroupSummary,
             bucket = bucket,
+            callType = sbn.toCallType(),
         )
 }
 
 private fun ActiveNotificationsStore.createOrReuse(
     key: String,
     groupKey: String?,
+    whenTime: Long,
     isAmbient: Boolean,
     isRowDismissed: Boolean,
     isSilent: Boolean,
@@ -155,14 +167,17 @@
     statusBarIcon: Icon?,
     uid: Int,
     packageName: String,
+    contentIntent: PendingIntent?,
     instanceId: Int?,
     isGroupSummary: Boolean,
     bucket: Int,
+    callType: CallType,
 ): ActiveNotificationModel {
     return individuals[key]?.takeIf {
         it.isCurrent(
             key = key,
             groupKey = groupKey,
+            whenTime = whenTime,
             isAmbient = isAmbient,
             isRowDismissed = isRowDismissed,
             isSilent = isSilent,
@@ -176,12 +191,15 @@
             instanceId = instanceId,
             isGroupSummary = isGroupSummary,
             packageName = packageName,
+            contentIntent = contentIntent,
             bucket = bucket,
+            callType = callType,
         )
     }
         ?: ActiveNotificationModel(
             key = key,
             groupKey = groupKey,
+            whenTime = whenTime,
             isAmbient = isAmbient,
             isRowDismissed = isRowDismissed,
             isSilent = isSilent,
@@ -195,13 +213,16 @@
             instanceId = instanceId,
             isGroupSummary = isGroupSummary,
             packageName = packageName,
+            contentIntent = contentIntent,
             bucket = bucket,
+            callType = callType,
         )
 }
 
 private fun ActiveNotificationModel.isCurrent(
     key: String,
     groupKey: String?,
+    whenTime: Long,
     isAmbient: Boolean,
     isRowDismissed: Boolean,
     isSilent: Boolean,
@@ -213,13 +234,16 @@
     statusBarIcon: Icon?,
     uid: Int,
     packageName: String,
+    contentIntent: PendingIntent?,
     instanceId: Int?,
     isGroupSummary: Boolean,
     bucket: Int,
+    callType: CallType,
 ): Boolean {
     return when {
         key != this.key -> false
         groupKey != this.groupKey -> false
+        whenTime != this.whenTime -> false
         isAmbient != this.isAmbient -> false
         isRowDismissed != this.isRowDismissed -> false
         isSilent != this.isSilent -> false
@@ -233,7 +257,9 @@
         instanceId != this.instanceId -> false
         isGroupSummary != this.isGroupSummary -> false
         packageName != this.packageName -> false
+        contentIntent != this.contentIntent -> false
         bucket != this.bucket -> false
+        callType != this.callType -> false
         else -> true
     }
 }
@@ -259,3 +285,13 @@
         else -> true
     }
 }
+
+private fun StatusBarNotification.toCallType(): CallType =
+    when (this.notification.extras.getInt(EXTRA_CALL_TYPE, -1)) {
+        -1 -> CallType.None
+        CALL_TYPE_INCOMING -> CallType.Incoming
+        CALL_TYPE_ONGOING -> CallType.Ongoing
+        CALL_TYPE_SCREENING -> CallType.Screening
+        CALL_TYPE_UNKNOWN -> CallType.Unknown
+        else -> CallType.Unknown
+    }
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 5527efc..6960791 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,6 +15,7 @@
 
 package com.android.systemui.statusbar.notification.shared
 
+import android.app.PendingIntent
 import android.graphics.drawable.Icon
 import com.android.systemui.statusbar.notification.stack.PriorityBucket
 
@@ -32,6 +33,8 @@
     val key: String,
     /** Notification group key associated with this entry. */
     val groupKey: String?,
+    /** When this notification was posted. */
+    val whenTime: Long,
     /** Is this entry in the ambient / minimized section (lowest priority)? */
     val isAmbient: Boolean,
     /**
@@ -60,12 +63,16 @@
     val uid: Int,
     /** The notifying app's packageName. */
     val packageName: String,
+    /** The intent to execute if UI related to this notification is clicked. */
+    val contentIntent: PendingIntent?,
     /** A small per-notification ID, used for statsd logging. */
     val instanceId: Int?,
     /** If this notification is the group summary for a group of notifications. */
     val isGroupSummary: Boolean,
     /** Indicates in which section the notification is displayed in. @see [PriorityBucket]. */
     @PriorityBucket val bucket: Int,
+    /** The call type set on the notification. */
+    val callType: CallType,
 ) : ActiveNotificationEntryModel()
 
 /** Model for a group of notifications. */
@@ -74,3 +81,17 @@
     val summary: ActiveNotificationModel,
     val children: List<ActiveNotificationModel>,
 ) : ActiveNotificationEntryModel()
+
+/** Specifies the call type set on the notification. For most notifications, will be [None]. */
+enum class CallType {
+    /** This notification isn't a call-type notification. */
+    None,
+    /** See [android.app.Notification.CallStyle.CALL_TYPE_INCOMING]. */
+    Incoming,
+    /** See [android.app.Notification.CallStyle.CALL_TYPE_ONGOING]. */
+    Ongoing,
+    /** See [android.app.Notification.CallStyle.CALL_TYPE_SCREENING]. */
+    Screening,
+    /** See [android.app.Notification.CallStyle.CALL_TYPE_UNKNOWN]. */
+    Unknown,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 3898088..7af0666 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -45,6 +45,9 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.statusbar.policy.CallbackController
@@ -65,6 +68,7 @@
     private val context: Context,
     private val ongoingCallRepository: OngoingCallRepository,
     private val notifCollection: CommonNotifCollection,
+    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
     private val systemClock: SystemClock,
     private val activityStarter: ActivityStarter,
     @Main private val mainExecutor: Executor,
@@ -97,6 +101,7 @@
             }
 
             override fun onEntryUpdated(entry: NotificationEntry) {
+                StatusBarUseReposForCallChip.assertInLegacyMode()
                 // We have a new call notification or our existing call notification has been
                 // updated.
                 // TODO(b/183229367): This likely won't work if you take a call from one app then
@@ -157,7 +162,25 @@
 
     override fun start() {
         dumpManager.registerDumpable(this)
-        notifCollection.addCollectionListener(notifListener)
+
+        if (Flags.statusBarUseReposForCallChip()) {
+            scope.launch {
+                // Listening to [ActiveNotificationsInteractor] instead of using
+                // [NotifCollectionListener#onEntryUpdated] is better for two reasons:
+                // 1. ActiveNotificationsInteractor automatically filters the notification list to
+                // just notifications for the current user, which ensures we don't show a call chip
+                // for User 1's call while User 2 is active (see b/328584859).
+                // 2. ActiveNotificationsInteractor only emits notifications that are currently
+                // present in the shade, which means we know we've already inflated the icon that we
+                // might use for the call chip (see b/354930838).
+                activeNotificationsInteractor.ongoingCallNotification.collect {
+                    updateInfoFromNotifModel(it)
+                }
+            }
+        } else {
+            notifCollection.addCollectionListener(notifListener)
+        }
+
         scope.launch {
             statusBarModeRepository.defaultDisplay.isInFullscreenMode.collect {
                 isFullscreen = it
@@ -221,6 +244,35 @@
         synchronized(mListeners) { mListeners.remove(listener) }
     }
 
+    private fun updateInfoFromNotifModel(notifModel: ActiveNotificationModel?) {
+        if (notifModel == null) {
+            removeChip()
+        } else if (notifModel.callType != CallType.Ongoing) {
+            logger.log(
+                TAG,
+                LogLevel.ERROR,
+                { str1 = notifModel.callType.name },
+                { "Notification Interactor sent ActiveNotificationModel with callType=$str1" },
+            )
+            removeChip()
+        } else {
+            val newOngoingCallInfo =
+                CallNotificationInfo(
+                    notifModel.key,
+                    notifModel.whenTime,
+                    notifModel.contentIntent,
+                    notifModel.uid,
+                    isOngoing = true,
+                    statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
+                )
+            if (newOngoingCallInfo == callNotificationInfo) {
+                return
+            }
+            callNotificationInfo = newOngoingCallInfo
+            updateChip()
+        }
+    }
+
     private fun updateChip() {
         val currentCallNotificationInfo = callNotificationInfo ?: return
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt
new file mode 100644
index 0000000..4bdd90e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.ongoingcall
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the status bar use repos for call chip flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object StatusBarUseReposForCallChip {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.statusBarUseReposForCallChip()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/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 277b887..572a0c1 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
@@ -15,6 +15,8 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.app.Notification
+import android.os.Bundle
 import android.service.notification.StatusBarNotification
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -139,9 +141,10 @@
 }
 
 private fun mockNotificationEntry(key: String, rank: Int = 0): NotificationEntry {
+    val mockNotification = mock<Notification> { this.extras = Bundle() }
     val mockSbn =
         mock<StatusBarNotification>() {
-            whenever(notification).thenReturn(mock())
+            whenever(notification).thenReturn(mockNotification)
             whenever(packageName).thenReturn("com.android")
         }
     return mock<NotificationEntry> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
index c4371fd..597e2e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
@@ -32,9 +32,11 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
+import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
@@ -44,6 +46,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.statusbar.window.StatusBarWindowController
@@ -84,7 +87,8 @@
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 @OptIn(ExperimentalCoroutinesApi::class)
-class OngoingCallControllerTest : SysuiTestCase() {
+@DisableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP)
+class OngoingCallControllerViaListenerTest : SysuiTestCase() {
     private val kosmos = Kosmos()
 
     private val clock = FakeSystemClock()
@@ -121,6 +125,7 @@
                 context,
                 ongoingCallRepository,
                 notificationCollection,
+                kosmos.activeNotificationsInteractor,
                 clock,
                 mockActivityStarter,
                 mainExecutor,
@@ -129,7 +134,7 @@
                 mockStatusBarWindowController,
                 mockSwipeStatusBarAwayGestureHandler,
                 statusBarModeRepository,
-                logcatLogBuffer("OngoingCallControllerTest"),
+                logcatLogBuffer("OngoingCallControllerViaListenerTest"),
             )
         controller.start()
         controller.addCallback(mockOngoingCallListener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
new file mode 100644
index 0000000..6c2e2c6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
@@ -0,0 +1,669 @@
+/*
+ * 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.phone.ongoingcall
+
+import android.app.ActivityManager
+import android.app.IActivityManager
+import android.app.IUidObserver
+import android.app.PendingIntent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
+import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
+import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.shared.CallType
+import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.time.fakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
+@EnableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP)
+class OngoingCallControllerViaRepoTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+
+    private val clock = kosmos.fakeSystemClock
+    private val mainExecutor = kosmos.fakeExecutor
+    private val testScope = kosmos.testScope
+    private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository
+    private val ongoingCallRepository = kosmos.ongoingCallRepository
+    private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+
+    private lateinit var controller: OngoingCallController
+
+    private val mockSwipeStatusBarAwayGestureHandler = mock<SwipeStatusBarAwayGestureHandler>()
+    private val mockOngoingCallListener = mock<OngoingCallListener>()
+    private val mockActivityStarter = kosmos.activityStarter
+    private val mockIActivityManager = mock<IActivityManager>()
+    private val mockStatusBarWindowController = mock<StatusBarWindowController>()
+
+    private lateinit var chipView: View
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        TestableLooper.get(this).runWithLooper {
+            chipView = LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip, null)
+        }
+
+        controller =
+            OngoingCallController(
+                testScope.backgroundScope,
+                context,
+                ongoingCallRepository,
+                mock<CommonNotifCollection>(),
+                kosmos.activeNotificationsInteractor,
+                clock,
+                mockActivityStarter,
+                mainExecutor,
+                mockIActivityManager,
+                DumpManager(),
+                mockStatusBarWindowController,
+                mockSwipeStatusBarAwayGestureHandler,
+                statusBarModeRepository,
+                logcatLogBuffer("OngoingCallControllerViaRepoTest"),
+            )
+        controller.start()
+        controller.addCallback(mockOngoingCallListener)
+        controller.setChipView(chipView)
+
+        // Let the controller get the starting value from activeNotificationsInteractor
+        testScope.runCurrent()
+        reset(mockOngoingCallListener)
+
+        whenever(
+                mockIActivityManager.getUidProcessState(
+                    eq(CALL_UID),
+                    any(),
+                )
+            )
+            .thenReturn(PROC_STATE_INVISIBLE)
+    }
+
+    @After
+    fun tearDown() {
+        controller.tearDownChipView()
+    }
+
+    @Test
+    fun interactorHasOngoingCallNotif_listenerAndRepoNotified() =
+        testScope.runTest {
+            setNotifOnRepo(
+                activeNotificationModel(
+                    key = "ongoingNotif",
+                    callType = CallType.Ongoing,
+                    uid = CALL_UID,
+                    whenTime = 567,
+                )
+            )
+
+            verify(mockOngoingCallListener).onOngoingCallStateChanged(any())
+            val repoState = ongoingCallRepository.ongoingCallState.value
+            assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
+            assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567)
+        }
+
+    @Test
+    fun notifRepoHasOngoingCallNotif_isOngoingCallNotif_windowControllerUpdated() {
+        setCallNotifOnRepo()
+
+        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true)
+    }
+
+    @Test
+    fun notifRepoHasNoCallNotif_listenerAndRepoNotNotified() {
+        setNoNotifsOnRepo()
+
+        verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(any())
+        assertThat(ongoingCallRepository.ongoingCallState.value)
+            .isInstanceOf(OngoingCallModel.NoCall::class.java)
+    }
+
+    @Test
+    fun notifRepoHasOngoingCallNotifThenScreeningNotif_listenerNotifiedTwice() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+            )
+        )
+
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Screening,
+            )
+        )
+
+        verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(any())
+    }
+
+    @Test
+    fun notifRepoHasOngoingCallNotifThenScreeningNotif_repoUpdated() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+            )
+        )
+
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Screening,
+            )
+        )
+
+        assertThat(ongoingCallRepository.ongoingCallState.value)
+            .isInstanceOf(OngoingCallModel.NoCall::class.java)
+    }
+
+    /** Regression test for b/191472854. */
+    @Test
+    fun notifRepoHasCallNotif_notifHasNullContentIntent_noCrash() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+                contentIntent = null,
+            )
+        )
+    }
+
+    /** Regression test for b/192379214. */
+    @Test
+    @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun notifRepoHasCallNotif_notificationWhenIsZero_timeHidden() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+                contentIntent = null,
+                whenTime = 0,
+            )
+        )
+
+        chipView.measure(
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+        )
+
+        assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
+            .isEqualTo(0)
+    }
+
+    @Test
+    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun notifRepoHasCallNotif_notificationWhenIsValid_timeShown() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+                whenTime = clock.currentTimeMillis(),
+            )
+        )
+
+        chipView.measure(
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+        )
+
+        assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
+            .isGreaterThan(0)
+    }
+
+    /** Regression test for b/194731244. */
+    @Test
+    fun repoHasCallNotif_updatedManyTimes_uidObserverOnlyRegisteredOnce() {
+        for (i in 0 until 4) {
+            // Re-create the notification each time so that it's considered a different object and
+            // will re-trigger the whole flow.
+            setNotifOnRepo(
+                activeNotificationModel(
+                    key = "notif$i",
+                    callType = CallType.Ongoing,
+                    whenTime = 44,
+                )
+            )
+        }
+
+        verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any())
+    }
+
+    /** Regression test for b/216248574. */
+    @Test
+    fun repoHasCallNotif_getUidProcessStateThrowsException_noCrash() {
+        whenever(
+                mockIActivityManager.getUidProcessState(
+                    eq(CALL_UID),
+                    any(),
+                )
+            )
+            .thenThrow(SecurityException())
+
+        // No assert required, just check no crash
+        setCallNotifOnRepo()
+    }
+
+    /** Regression test for b/216248574. */
+    @Test
+    fun repoHasCallNotif_registerUidObserverThrowsException_noCrash() {
+        whenever(
+                mockIActivityManager.registerUidObserver(
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                )
+            )
+            .thenThrow(SecurityException())
+
+        // No assert required, just check no crash
+        setCallNotifOnRepo()
+    }
+
+    /** Regression test for b/216248574. */
+    @Test
+    fun repoHasCallNotif_packageNameProvidedToActivityManager() {
+        setCallNotifOnRepo()
+
+        val packageNameCaptor = argumentCaptor<String>()
+        verify(mockIActivityManager)
+            .registerUidObserver(any(), any(), any(), packageNameCaptor.capture())
+        assertThat(packageNameCaptor.firstValue).isNotNull()
+    }
+
+    @Test
+    fun repo_callNotifAddedThenRemoved_listenerNotified() {
+        setCallNotifOnRepo()
+        reset(mockOngoingCallListener)
+
+        setNoNotifsOnRepo()
+
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(any())
+    }
+
+    @Test
+    fun repo_callNotifAddedThenRemoved_repoUpdated() {
+        setCallNotifOnRepo()
+
+        setNoNotifsOnRepo()
+
+        assertThat(ongoingCallRepository.ongoingCallState.value)
+            .isInstanceOf(OngoingCallModel.NoCall::class.java)
+    }
+
+    @Test
+    fun repo_callNotifAddedThenRemoved_windowControllerUpdated() {
+        reset(mockStatusBarWindowController)
+
+        setCallNotifOnRepo()
+
+        setNoNotifsOnRepo()
+
+        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false)
+    }
+
+    @Test
+    fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() {
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
+    fun hasOngoingCall_repoHasUnrelatedNotif_returnsFalse() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "unrelated",
+                callType = CallType.None,
+                uid = CALL_UID,
+            )
+        )
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
+    fun hasOngoingCall_repoHasScreeningCall_returnsFalse() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "unrelated",
+                callType = CallType.Screening,
+                uid = CALL_UID,
+            )
+        )
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
+    fun hasOngoingCall_repoHasCallNotifAndCallAppNotVisible_returnsTrue() {
+        whenever(
+                mockIActivityManager.getUidProcessState(
+                    eq(CALL_UID),
+                    any(),
+                )
+            )
+            .thenReturn(PROC_STATE_INVISIBLE)
+
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+                uid = CALL_UID,
+            )
+        )
+
+        assertThat(controller.hasOngoingCall()).isTrue()
+    }
+
+    @Test
+    fun hasOngoingCall_repoHasCallNotifButCallAppVisible_returnsFalse() {
+        whenever(mockIActivityManager.getUidProcessState(eq(CALL_UID), any()))
+            .thenReturn(PROC_STATE_VISIBLE)
+
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+                uid = CALL_UID,
+            )
+        )
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
+    fun hasOngoingCall_repoHasCallNotifButInvalidChipView_returnsFalse() {
+        val invalidChipView = LinearLayout(context)
+        controller.setChipView(invalidChipView)
+
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                callType = CallType.Ongoing,
+                uid = CALL_UID,
+            )
+        )
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
+    fun hasOngoingCall_repoHasCallNotifThenDoesNot_returnsFalse() {
+        setCallNotifOnRepo()
+        setNoNotifsOnRepo()
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
+    fun hasOngoingCall_repoHasCallNotifThenScreeningNotif_returnsFalse() {
+        setCallNotifOnRepo()
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "screening",
+                callType = CallType.Screening,
+                uid = CALL_UID,
+            )
+        )
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    /**
+     * This test fakes a theme change during an ongoing call.
+     *
+     * When a theme change happens,
+     * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] and its views get
+     * re-created, so [OngoingCallController.setChipView] gets called with a new view. If there's an
+     * active ongoing call when the theme changes, the new view needs to be updated with the call
+     * information.
+     */
+    @Test
+    fun setChipView_whenRepoHasOngoingCall_listenerNotifiedWithNewView() {
+        // Start an ongoing call.
+        setCallNotifOnRepo()
+        reset(mockOngoingCallListener)
+
+        lateinit var newChipView: View
+        TestableLooper.get(this).runWithLooper {
+            newChipView =
+                LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip, null)
+        }
+
+        // Change the chip view associated with the controller.
+        controller.setChipView(newChipView)
+
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(any())
+    }
+
+    @Test
+    fun callProcessChangesToVisible_listenerNotified() {
+        // Start the call while the process is invisible.
+        whenever(mockIActivityManager.getUidProcessState(eq(CALL_UID), any()))
+            .thenReturn(PROC_STATE_INVISIBLE)
+        setCallNotifOnRepo()
+        reset(mockOngoingCallListener)
+
+        val captor = argumentCaptor<IUidObserver.Stub>()
+        verify(mockIActivityManager).registerUidObserver(captor.capture(), any(), any(), any())
+        val uidObserver = captor.firstValue
+
+        // Update the process to visible.
+        uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0)
+        mainExecutor.advanceClockToLast()
+        mainExecutor.runAllReady()
+
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(any())
+    }
+
+    @Test
+    fun callProcessChangesToInvisible_listenerNotified() {
+        // Start the call while the process is visible.
+        whenever(mockIActivityManager.getUidProcessState(eq(CALL_UID), any()))
+            .thenReturn(PROC_STATE_VISIBLE)
+        setCallNotifOnRepo()
+        reset(mockOngoingCallListener)
+
+        val captor = argumentCaptor<IUidObserver.Stub>()
+        verify(mockIActivityManager).registerUidObserver(captor.capture(), any(), any(), any())
+        val uidObserver = captor.firstValue
+
+        // Update the process to invisible.
+        uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0)
+        mainExecutor.advanceClockToLast()
+        mainExecutor.runAllReady()
+
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(any())
+    }
+
+    /** Regression test for b/212467440. */
+    @Test
+    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() {
+        val pendingIntent = mock<PendingIntent>()
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "notif",
+                uid = CALL_UID,
+                contentIntent = pendingIntent,
+                callType = CallType.Ongoing,
+            )
+        )
+
+        chipView.performClick()
+
+        // Ensure that the sysui didn't modify the notification's intent -- see b/212467440.
+        verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
+    }
+
+    @Test
+    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun callNotificationAdded_chipIsClickable() {
+        setCallNotifOnRepo()
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun callNotificationAdded_newChipsEnabled_chipNotClickable() {
+        setCallNotifOnRepo()
+
+        assertThat(chipView.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun fullscreenIsTrue_chipStillClickable() {
+        setCallNotifOnRepo()
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
+        testScope.runCurrent()
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
+        testScope.runCurrent()
+
+        setCallNotifOnRepo()
+
+        verify(mockSwipeStatusBarAwayGestureHandler).addOnGestureDetectedCallback(any(), any())
+    }
+
+    @Test
+    fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() {
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
+        testScope.runCurrent()
+
+        setCallNotifOnRepo()
+
+        verify(mockSwipeStatusBarAwayGestureHandler, never())
+            .addOnGestureDetectedCallback(any(), any())
+    }
+
+    @Test
+    fun transitionToImmersiveMode_swipeGestureCallbackAdded() {
+        setCallNotifOnRepo()
+
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
+        testScope.runCurrent()
+
+        verify(mockSwipeStatusBarAwayGestureHandler).addOnGestureDetectedCallback(any(), any())
+    }
+
+    @Test
+    fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() {
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
+        testScope.runCurrent()
+
+        setCallNotifOnRepo()
+        reset(mockSwipeStatusBarAwayGestureHandler)
+
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
+        testScope.runCurrent()
+
+        verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(any())
+    }
+
+    @Test
+    fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() {
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
+        testScope.runCurrent()
+        setCallNotifOnRepo()
+        reset(mockSwipeStatusBarAwayGestureHandler)
+
+        setNoNotifsOnRepo()
+
+        verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(any())
+    }
+
+    private fun setCallNotifOnRepo() {
+        setNotifOnRepo(
+            activeNotificationModel(
+                key = "ongoingNotif",
+                callType = CallType.Ongoing,
+                uid = CALL_UID,
+                contentIntent = mock<PendingIntent>(),
+            )
+        )
+    }
+
+    private fun setNotifOnRepo(notif: ActiveNotificationModel) {
+        activeNotificationListRepository.activeNotifications.value =
+            ActiveNotificationsStore.Builder().apply { addIndividualNotif(notif) }.build()
+        testScope.runCurrent()
+    }
+
+    private fun setNoNotifsOnRepo() {
+        activeNotificationListRepository.activeNotifications.value =
+            ActiveNotificationsStore.Builder().build()
+        testScope.runCurrent()
+    }
+}
+
+private const val CALL_UID = 900
+
+// A process state that represents the process being visible to the user.
+private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP
+
+// A process state that represents the process being invisible to the user.
+private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
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 9c5c486..37f1f13 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
@@ -16,14 +16,17 @@
 
 package com.android.systemui.statusbar.notification.data.model
 
+import android.app.PendingIntent
 import android.graphics.drawable.Icon
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN
 
 /** Simple ActiveNotificationModel builder for use in tests. */
 fun activeNotificationModel(
     key: String,
     groupKey: String? = null,
+    whenTime: Long = 0L,
     isAmbient: Boolean = false,
     isRowDismissed: Boolean = false,
     isSilent: Boolean = false,
@@ -37,11 +40,14 @@
     instanceId: Int? = null,
     isGroupSummary: Boolean = false,
     packageName: String = "pkg",
+    contentIntent: PendingIntent? = null,
     bucket: Int = BUCKET_UNKNOWN,
+    callType: CallType = CallType.None,
 ) =
     ActiveNotificationModel(
         key = key,
         groupKey = groupKey,
+        whenTime = whenTime,
         isAmbient = isAmbient,
         isRowDismissed = isRowDismissed,
         isSilent = isSilent,
@@ -53,7 +59,9 @@
         statusBarIcon = statusBarIcon,
         uid = uid,
         packageName = packageName,
+        contentIntent = contentIntent,
         instanceId = instanceId,
         isGroupSummary = isGroupSummary,
         bucket = bucket,
+        callType = callType,
     )