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

Commit 914ea599 authored by Lyn Han's avatar Lyn Han Committed by Android (Google) Code Review
Browse files

Merge changes I2fbce23f,I6c23c806 into main

* changes:
  StackCoordinator: notif stats for bundles
  Make StackCoordinatorTest deviceless
parents fa7383cf 520c2d7a
Loading
Loading
Loading
Loading
+364 −0
Original line number Diff line number Diff line
@@ -15,23 +15,37 @@
 */
package com.android.systemui.statusbar.notification.collection.coordinator

import android.app.Notification
import android.content.Context
import android.os.UserHandle
import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX
import com.android.systemui.SysuiTestCase
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.InternalNotificationsApi
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
import com.android.systemui.statusbar.notification.data.model.NotifStats
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.data.repository.TEST_BUNDLE_SPEC
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
@@ -51,17 +65,31 @@ class StackCoordinatorTest : SysuiTestCase() {
    private lateinit var entry: NotificationEntry
    private lateinit var coordinator: StackCoordinator
    private lateinit var afterRenderListListener: OnAfterRenderListListener
    private lateinit var testUtil: TestUtil

    private val pipeline: NotifPipeline = mock()
    private val groupExpansionManagerImpl: GroupExpansionManagerImpl = mock()
    private val renderListInteractor: RenderNotificationListInteractor = mock()
    private val activeNotificationsInteractor: ActiveNotificationsInteractor = mock()
    private val sensitiveNotificationProtectionController:
        SensitiveNotificationProtectionController =
    private val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController =
        mock()
    private val section: NotifSection = mock()
    private val row: ExpandableNotificationRow = mock()

    private val alertingSectioner: NotifSectioner = mock()
    private val silentSectioner: NotifSectioner = mock()
    private lateinit var alertingSection: NotifSection
    private lateinit var silentSection: NotifSection

    private val clearableAlerting =
        TestUtil.TestEntry("clearableAlerting", isSilent = false, isClearable = true)
    private val nonClearableAlerting =
        TestUtil.TestEntry("nonClearableAlerting", isSilent = false, isClearable = false)
    private val clearableSilent =
        TestUtil.TestEntry("clearableSilent", isSilent = true, isClearable = true)
    private val nonClearableSilent =
        TestUtil.TestEntry("nonClearableSilent", isSilent = true, isClearable = false)

    @Before
    fun setUp() {
        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false)
@@ -69,6 +97,17 @@ class StackCoordinatorTest : SysuiTestCase() {
        entry = NotificationEntryBuilder().setSection(section).build()
        entry.row = row
        entry.setSensitive(false, false)

        whenever(alertingSectioner.bucket).thenReturn(BUCKET_ALERTING)
        whenever(alertingSectioner.comparator).thenReturn(mock<NotifComparator>())
        alertingSection = NotifSection(alertingSectioner, 0)

        whenever(silentSectioner.bucket).thenReturn(BUCKET_SILENT)
        whenever(silentSectioner.comparator).thenReturn(mock<NotifComparator>())
        silentSection = NotifSection(silentSectioner, 1)

        testUtil = TestUtil(context, alertingSection, silentSection)

        coordinator =
            StackCoordinator(
                groupExpansionManagerImpl,
@@ -167,4 +206,159 @@ class StackCoordinatorTest : SysuiTestCase() {
                )
            )
    }

    @Test
    fun stats_forBundle_setsAlertingFlags() {
        val bundle = testUtil.buildBundle(
            listOf(
                testUtil.buildEntry(clearableAlerting),
                testUtil.buildEntry(nonClearableAlerting),
            )
        )
        runTestAndAssertStats(
            listOf(bundle),
            hasClearableAlertingNotifs = true,
            hasNonClearableAlertingNotifs = true
        )
    }


    @Test
    fun stats_forBundle_setsSilentFlags() {
        val bundle = testUtil.buildBundle(
            listOf(
                testUtil.buildEntry(clearableSilent),
                testUtil.buildEntry(nonClearableSilent),
            )
        )
        runTestAndAssertStats(
            listOf(bundle),
            hasClearableSilentNotifs = true,
            hasNonClearableSilentNotifs = true
        )
    }

    @Test
    fun stats_forEmptyGroupAndBundle_setsNoFlags() {
        val emptyGroup = testUtil.buildGroup("emptyGroup", emptyList())
        val emptyBundle = testUtil.buildBundle(emptyList())
        runTestAndAssertStats(listOf(emptyGroup, emptyBundle))
    }

    @Test
    fun stats_forMix_setsAllFlags() {
        val alertingBundle = testUtil.buildBundle(listOf(testUtil.buildEntry(clearableAlerting)))
        val silentGroup = testUtil.buildGroup("g1", listOf(testUtil.buildEntry(clearableSilent)))
        val nonClearableAlertingEntry = testUtil.buildEntry(nonClearableAlerting)
        val nonClearableSilentEntry = testUtil.buildEntry(nonClearableSilent)

        runTestAndAssertStats(
            listOf(
                alertingBundle,
                silentGroup,
                nonClearableAlertingEntry,
                nonClearableSilentEntry,
            ),
            hasClearableAlertingNotifs = true,
            hasNonClearableAlertingNotifs = true,
            hasClearableSilentNotifs = true,
            hasNonClearableSilentNotifs = true
        )
    }

    private fun runTestAndAssertStats(
        entries: List<PipelineEntry>,
        hasClearableAlertingNotifs: Boolean = false,
        hasNonClearableAlertingNotifs: Boolean = false,
        hasClearableSilentNotifs: Boolean = false,
        hasNonClearableSilentNotifs: Boolean = false,
    ) {
        afterRenderListListener.onAfterRenderList(entries)
        verify(activeNotificationsInteractor).setNotifStats(
            NotifStats(
                hasClearableAlertingNotifs = hasClearableAlertingNotifs,
                hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs,
                hasClearableSilentNotifs = hasClearableSilentNotifs,
                hasNonClearableSilentNotifs = hasNonClearableSilentNotifs,
            )
        )
    }

    @OptIn(InternalNotificationsApi::class)
    private class TestUtil(
        private val context: Context,
        private val alertingSection: NotifSection,
        private val silentSection: NotifSection,
    ) {
        private val testPkg = "testPkg"
        private val testUid = 12345

        /** Hold data for testing */
        data class TestEntry(val key: String, val isSilent: Boolean, val isClearable: Boolean)

        private fun createSbn(key: String): StatusBarNotification {
            val notification = Notification.Builder(context, "test_channel_id")
                .setSmallIcon(R.drawable.ic_person)
                .setContentTitle("Title for $key")
                .build()

            // Use test key's hashcode as notif ID to ensure SBN key is unique for each test entry
            return StatusBarNotification(
                testPkg,
                testPkg,
                key.hashCode(),
                null, // tag
                testUid,
                0, // initialPid
                notification,
                UserHandle.of(0),
                null, // overrideGroupKey
                0L // postTime
            )
        }

        /** Builds NotificationEntry based on TestEntry */
        fun buildEntry(spec: TestEntry): NotificationEntry {
            val sbn = createSbn(spec.key)

            if (!spec.isClearable) {
                // Make non-clearable by making it ongoing
                sbn.notification.flags = sbn.notification.flags or Notification.FLAG_ONGOING_EVENT
            }

            return NotificationEntryBuilder(context)
                .setSbn(sbn)
                .setSection(if (spec.isSilent) silentSection else alertingSection)
                .build()
                .apply {
                    // isClearable requires non-null row
                    row = mock<ExpandableNotificationRow>()
                    setSensitive(/* sensitive= */ false, /* deviceSensitive= */ false)
                }
        }

        /** Builds a real GroupEntry using a builder */
        fun buildGroup(key: String, children: List<NotificationEntry>): GroupEntry {
            val builder = GroupEntryBuilder()
                .setKey(key)
                .setChildren(children)

            // Only create a summary if the group has children
            if (children.isNotEmpty()) {
                val summary = buildEntry(TestEntry(key, isSilent = false, isClearable = false))
                builder.setSummary(summary)
            }

            return builder.build()
        }

        /**
         * Builds a real BundleEntry using a test spec
         */
        fun buildBundle(children: List<ListEntry>): BundleEntry {
            val bundle = BundleEntry(TEST_BUNDLE_SPEC)
            children.forEach { bundle.addChild(it) }
            return bundle
        }
    }
}
 No newline at end of file
+75 −15
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

package com.android.systemui.statusbar.notification.collection.coordinator

import android.util.Log
import com.android.app.tracing.traceSection
import com.android.server.notification.Flags.screenshareNotificationHiding
import com.android.systemui.Flags.screenshareNotificationHidingBugFix
import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
@@ -31,6 +34,8 @@ import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
import javax.inject.Inject

private const val TAG = "StackCoordinator"

/**
 * A small coordinator which updates the notif stack (the view layer which holds notifications) with
 * high-level data after the stack is populated with the final entries.
@@ -38,7 +43,7 @@ import javax.inject.Inject
@CoordinatorScope
class StackCoordinator
@Inject
internal constructor(
constructor(
    private val groupExpansionManagerImpl: GroupExpansionManagerImpl,
    private val renderListInteractor: RenderNotificationListInteractor,
    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
@@ -57,30 +62,35 @@ internal constructor(
            renderListInteractor.setRenderedList(entries)
        }

    /**
     * Calculates stats about the notification list. This implementation first recursively unpacks
     * all containers into a single, flat list of every individual notification, then iterates over
     * that list to compute the final stats.
     */
    private fun calculateNotifStats(entries: List<PipelineEntry>): NotifStats {
        var hasNonClearableAlertingNotifs = false
        var hasClearableAlertingNotifs = false
        var hasNonClearableSilentNotifs = false
        var hasClearableSilentNotifs = false

        val isSensitiveContentProtectionActive =
            screenshareNotificationHiding() &&
                    screenshareNotificationHidingBugFix() &&
                    sensitiveNotificationProtectionController.isSensitiveStateActive
        entries.forEach {
            if (it is BundleEntry) {
                // TODO(b/399736937) calculate based on notifs inside bundle
                return@forEach
            }
            val section = checkNotNull(it.section) { "Null section for ${it.key}" }
            val entry =
                checkNotNull(it.asListEntry()?.representativeEntry) {
                    "Null notif entry for ${it.key}"

        val notifEntryList = getFlatNotifEntryList(entries)
        for (entry in notifEntryList) {
            // Once all four booleans are true, we have all the info we need and can stop iterating
            if (hasNonClearableAlertingNotifs && hasClearableAlertingNotifs &&
                hasNonClearableSilentNotifs && hasClearableSilentNotifs) {
                break
            }

            val section = checkNotNull(entry.section) { "Null section for ${entry.key}" }
            val isSilent = section.bucket == BUCKET_SILENT
            // NOTE: NotificationEntry.isClearable will internally check group children to ensure
            //  the group itself definitively clearable.
            val isClearable =
                !isSensitiveContentProtectionActive && entry.isClearable && !entry.isSensitive.value

            when {
                isSilent && isClearable -> hasClearableSilentNotifs = true
                isSilent && !isClearable -> hasNonClearableSilentNotifs = true
@@ -88,6 +98,7 @@ internal constructor(
                else -> hasNonClearableAlertingNotifs = true
            }
        }

        return NotifStats(
            hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs,
            hasClearableAlertingNotifs = hasClearableAlertingNotifs,
@@ -95,4 +106,53 @@ internal constructor(
            hasClearableSilentNotifs = hasClearableSilentNotifs,
        )
    }

    /**
     * Recursively traverses list of PipelineEntry to unpack all containers and return a
     * flat list of NotificationEntry
     */
    private fun getFlatNotifEntryList(entries: List<PipelineEntry>): List<NotificationEntry> {
        return buildList {
            for (entry in entries) {
                when (entry) {
                    is NotificationEntry -> {
                        add(entry)
                    }
                    is GroupEntry -> {
                        // NOTE: We must process the children of a group directly instead of relying
                        // on the summary's isClearable() method.
                        //
                        // The summary's isClearable() only tells us if ALL children are clearable
                        // and hides the fact that a group can contain a mix of clearable and
                        // non-clearable notifications.
                        //
                        // Consider a group with:
                        //  - one clearable alerting notif
                        //  - one NON-clearable notif
                        //  The summary's isClearable() would be false. If we only check the
                        //  summary, we miss the clearable notif and incorrectly set
                        //  hasClearableAlertingNotifs to false
                        if (entry.children.isNotEmpty()) {
                            addAll(entry.children)
                            if (entry.summary == null) {
                                Log.w(TAG, "Group ${entry.key} has children but no summary")
                            }
                        } else {
                            entry.summary?.let { add(it) }
                        }
                    }
                    is BundleEntry -> {
                        addAll(getFlatNotifEntryList(entry.children))
                    }
                    else -> {
                        Log.w(
                            TAG,
                            "Unknown PipelineEntry type: ${entry::class.simpleName}" +
                                    "with key ${entry.key}"
                        )
                    }
                }
            }
        }
    }
}
 No newline at end of file