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

Commit 1c7aa0df authored by Steve Elliott's avatar Steve Elliott Committed by Android (Google) Code Review
Browse files

Merge changes I455ffbfd,If1db1f96 into main

* changes:
  Fix IOOB when stabilizing bundle group children
  Move count traversal to BundleCoordinator
parents efcc8a58 f199a60d
Loading
Loading
Loading
Loading
+0 −137
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.collection

import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.row.data.repository.TEST_BUNDLE_SPEC
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@EnableFlags(NotificationBundleUi.FLAG_NAME)
class BundleEntryTest : SysuiTestCase() {
    private lateinit var bundleEntry: BundleEntry
    @get:Rule val setFlagsRule = SetFlagsRule()

    @Before
    fun setUp() {
        bundleEntry = BundleEntry(TEST_BUNDLE_SPEC)
    }

    @Test
    fun testTotalCount_initial_isZero() {
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(0)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addNotif() {
        bundleEntry.addChild(NotificationEntryBuilder().build())
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(1)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addGroup() {
        val groupEntry1 = GroupEntry("key", 0)
        groupEntry1.addChild(NotificationEntryBuilder().build())
        groupEntry1.addChild(NotificationEntryBuilder().build())
        bundleEntry.addChild(groupEntry1)
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(2)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addMultipleGroups() {
        val groupEntry1 = GroupEntry("key", 0)
        groupEntry1.addChild(NotificationEntryBuilder().build())
        bundleEntry.addChild(groupEntry1)

        val groupEntry2 = GroupEntry("key", 0)
        groupEntry2.addChild(NotificationEntryBuilder().build())
        groupEntry2.addChild(NotificationEntryBuilder().build())
        bundleEntry.addChild(groupEntry2)

        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(3)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addNotifAndGroup() {
        val groupEntry1 = GroupEntry("key", 0)
        groupEntry1.addChild(NotificationEntryBuilder().build())
        bundleEntry.addChild(groupEntry1)
        bundleEntry.addChild(NotificationEntryBuilder().build())
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(2)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_removeNotif() {
        val groupEntry1 = GroupEntry("key", 0)
        groupEntry1.addChild(NotificationEntryBuilder().build())
        bundleEntry.addChild(groupEntry1)

        val bundleNotifChild = NotificationEntryBuilder().build()
        bundleEntry.addChild(bundleNotifChild)
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(2)

        bundleEntry.removeChild(bundleNotifChild)
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(1)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_removeGroupChild() {
        val groupEntry1 = GroupEntry("key", 0)
        groupEntry1.addChild(NotificationEntryBuilder().build())
        bundleEntry.addChild(groupEntry1)
        bundleEntry.addChild(NotificationEntryBuilder().build())
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(2)

        groupEntry1.clearChildren()

        // Explicitly call updateTotalCount, which is what ShadeListBuilder does via
        // BundleCoordinator's OnBeforeRenderListListener before rendering.
        bundleEntry.updateTotalCount()
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(1)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_clearChildren() {
        val groupEntry1 = GroupEntry("key", 0)
        groupEntry1.addChild(NotificationEntryBuilder().build())
        bundleEntry.addChild(groupEntry1)
        bundleEntry.addChild(NotificationEntryBuilder().build())
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(2)

        bundleEntry.clearChildren()
        assertThat(bundleEntry.bundleRepository.numberOfChildren).isEqualTo(0)
    }
}
+118 −0
Original line number Diff line number Diff line
@@ -168,6 +168,124 @@ class BundleCoordinatorTest : SysuiTestCase() {
            .containsExactly(AppData(pkg1, user1), AppData(pkg2, user2))
    }

    @Test
    fun testTotalCount_initial_isZero() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)

        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(0)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addNotif() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)
        bundle.addChild(NotificationEntryBuilder().build())

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(1)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addGroup() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)
        val groupEntry = GroupEntry("groupKey1", 0L)
        groupEntry.rawChildren.add(NotificationEntryBuilder().build())
        groupEntry.rawChildren.add(NotificationEntryBuilder().build())
        bundle.addChild(groupEntry)

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(2)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addMultipleGroups() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)

        val groupEntry1 = GroupEntry("groupKey1", 0L)
        groupEntry1.rawChildren.add(NotificationEntryBuilder().build())
        bundle.addChild(groupEntry1)

        val groupEntry2 = GroupEntry("groupKey2", 0L)
        groupEntry2.rawChildren.add(NotificationEntryBuilder().build())
        groupEntry2.rawChildren.add(NotificationEntryBuilder().build())
        bundle.addChild(groupEntry2)

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(3)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_addNotifAndGroup() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)

        val groupEntry1 = GroupEntry("groupKey1", 0L)
        groupEntry1.rawChildren.add(NotificationEntryBuilder().build())
        bundle.addChild(groupEntry1)
        bundle.addChild(NotificationEntryBuilder().build())

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(2)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_removeNotif() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)

        val directNotifChild = NotificationEntryBuilder().build()
        bundle.addChild(directNotifChild)

        val groupEntry1 = GroupEntry("groupKey1", 0L)
        groupEntry1.rawChildren.add(NotificationEntryBuilder().build())
        bundle.addChild(groupEntry1)

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(2)

        bundle.removeChild(directNotifChild)

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(1)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_removeGroupChild() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)
        val groupEntry1 = GroupEntry("key", 0)
        groupEntry1.rawChildren.add(NotificationEntryBuilder().build())
        bundle.addChild(groupEntry1)
        bundle.addChild(NotificationEntryBuilder().build())

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(2)

        groupEntry1.rawChildren.clear()
        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(1)
    }

    @OptIn(InternalNotificationsApi::class)
    @Test
    fun testTotalCount_clearChildren() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)
        val groupEntry1 = GroupEntry("groupKey1", 0L)
        groupEntry1.rawChildren.add(NotificationEntryBuilder().build())
        bundle.addChild(groupEntry1)
        bundle.addChild(NotificationEntryBuilder().build())

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(2)

        bundle.clearChildren()

        coordinator.bundleCountUpdater.onBeforeRenderList(listOf(bundle))
        assertThat(bundle.bundleRepository.numberOfChildren).isEqualTo(0)
    }

    private fun makeEntryOfChannelType(
        type: String,
        buildBlock: NotificationEntryBuilder.() -> Unit = {},
+0 −24
Original line number Diff line number Diff line
@@ -15,7 +15,6 @@
 */
package com.android.systemui.statusbar.notification.collection

import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.data.repository.BundleRepository
import java.util.Collections
@@ -56,19 +55,16 @@ class BundleEntry(spec: BundleSpec) : PipelineEntry(spec.key) {
    @InternalNotificationsApi
    fun addChild(child: ListEntry) {
        _children.add(child)
        updateTotalCount()
    }

    @InternalNotificationsApi
    fun removeChild(child: ListEntry) {
        _children.remove(child)
        updateTotalCount()
    }

    @InternalNotificationsApi
    fun clearChildren() {
        _children.clear()
        updateTotalCount()
    }

    override fun asListEntry(): ListEntry? {
@@ -86,24 +82,4 @@ class BundleEntry(spec: BundleSpec) : PipelineEntry(spec.key) {
     */
    val isClearable: Boolean
        get() = _children.all { it.representativeEntry?.sbn?.isClearable != false }

    /**
     * The total count of [NotificationEntry]s within bundle. Notification updates trigger pipeline
     * rebuilds, so updates to group children will be reflected in this count.
     */
    @VisibleForTesting
    fun updateTotalCount() {
        var count = 0
        for (child in _children) {
            when (child) {
                is NotificationEntry -> {
                    count++
                }
                is GroupEntry -> {
                    count += child.getChildren().size
                }
            }
        }
        bundleRepository.numberOfChildren = count
    }
}
+2 −4
Original line number Diff line number Diff line
@@ -803,8 +803,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
                // maybe put bundle children back into their old parents (including moving back to
                // top-level)
                final List<ListEntry> bundleChildren = bundleEntry.getRawChildren();
                final int bundleSize = bundleChildren.size();
                for (int j = 0; j < bundleSize; j++) {
                for (int j = 0; j < bundleChildren.size(); j++) {
                    final ListEntry child = bundleChildren.get(j);
                    if (maybeSuppressParentChange(child, topLevelList)) {
                        bundleChildren.remove(j);
@@ -814,8 +813,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
                        // maybe put group children back into their old parents (including moving
                        // back to top-level)
                        final List<NotificationEntry> groupChildren = groupEntry.getRawChildren();
                        final int groupSize = groupChildren.size();
                        for (int k = 0; k < groupSize; k++) {
                        for (int k = 0; k < groupChildren.size(); k++) {
                            if (maybeSuppressParentChange(groupChildren.get(k), topLevelList)) {
                                // child was put back into its previous parent, so we remove it from
                                // this group
+31 −14
Original line number Diff line number Diff line
@@ -153,11 +153,7 @@ constructor(
        }

    private fun inflateAllBundleEntries(entries: List<PipelineEntry>) {
        for (entry in entries) {
            if (entry is BundleEntry) {
                bundleBarn.inflateBundleEntry(entry)
            }
        }
        entries.forEachBundleEntry { bundleBarn.inflateBundleEntry(it) }
    }

    private val bundleFilter: NotifFilter =
@@ -177,11 +173,25 @@ constructor(
            }
        }

    private val bundleCountUpdater = OnBeforeRenderListListener { entries ->
        for (entry in entries) {
            if (entry is BundleEntry) {
                entry.updateTotalCount()
    /** Updates the total count of [NotificationEntry]s within each bundle. */
    @get:VisibleForTesting
    val bundleCountUpdater = OnBeforeRenderListListener { entries ->
        entries.forEachBundleEntry { bundleEntry ->
            var count = 0
            for (child in bundleEntry.children) {
                when (child) {
                    is NotificationEntry -> {
                        count++
                    }

                    is GroupEntry -> {
                        count += child.children.size
                    }

                    else -> error("Unexpected ListEntry type: ${child::class.simpleName}")
                }
            }
            bundleEntry.bundleRepository.numberOfChildren = count
        }
    }

@@ -191,10 +201,9 @@ constructor(
     */
    @get:VisibleForTesting
    val bundleAppDataUpdater = OnBeforeRenderListListener { entries ->
        for (entry in entries) {
            if (entry !is BundleEntry) continue
        entries.forEachBundleEntry { bundleEntry ->
            val newAppDataList: List<AppData> =
                entry.children.flatMap { listEntry ->
                bundleEntry.children.flatMap { listEntry ->
                    when (listEntry) {
                        is NotificationEntry -> {
                            listOf(AppData(listEntry.sbn.packageName, listEntry.sbn.user))
@@ -206,7 +215,7 @@ constructor(
                                listOf(AppData(summary.sbn.packageName, summary.sbn.user))
                            } else {
                                error(
                                    "BundleEntry (key: ${entry.key}) contains GroupEntry " +
                                    "BundleEntry (key: ${bundleEntry.key}) contains GroupEntry " +
                                        "(key: ${listEntry.key}) with no summary."
                                )
                            }
@@ -215,7 +224,7 @@ constructor(
                        else -> error("Unexpected ListEntry type: ${listEntry::class.simpleName}")
                    }
                }
            entry.bundleRepository.appDataList = newAppDataList
            bundleEntry.bundleRepository.appDataList = newAppDataList
        }
    }

@@ -229,6 +238,14 @@ constructor(
        }
    }

    private fun List<PipelineEntry>.forEachBundleEntry(block: (BundleEntry) -> Unit) {
        for (entry in this) {
            if (entry is BundleEntry) {
                block(entry)
            }
        }
    }

    companion object {
        @JvmField val TAG: String = "BundleCoordinator"