Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b2e6859a authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[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
parent 2c4b5b9c
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -391,6 +391,17 @@ flag {
    bug: "332662551"
}

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"
+114 −0
Original line number Diff line number Diff line
@@ -24,8 +24,11 @@ import com.android.systemui.SysuiTestCase
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
@@ -55,6 +58,117 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
            assertThat(underTest.allNotificationsCountValue).isEqualTo(5)
        }

    @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 {
+16 −0
Original line number Diff line number Diff line
@@ -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.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map

@SysUISingleton
class ActiveNotificationsInteractor
@Inject
constructor(
@@ -71,6 +74,19 @@ constructor(
    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
+36 −0
Original line number Diff line number Diff line
@@ -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.data.repository.ActiveNotific
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 @@ private class ActiveNotificationsStoreBuilder(
        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 @@ private class ActiveNotificationsStoreBuilder(
            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 @@ private fun ActiveNotificationsStore.createOrReuse(
    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 @@ private fun ActiveNotificationsStore.createOrReuse(
            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 @@ private fun ActiveNotificationsStore.createOrReuse(
            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 @@ private fun ActiveNotificationModel.isCurrent(
    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 @@ private fun ActiveNotificationModel.isCurrent(
        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 @@ private fun ActiveNotificationGroupModel.isCurrent(
        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
    }
+21 −0
Original line number Diff line number Diff line
@@ -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 @@ data class ActiveNotificationModel(
    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 @@ data class ActiveNotificationModel(
    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 @@ data class ActiveNotificationGroupModel(
    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,
}
Loading