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

Commit 036e0e50 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Show app icons in bundle header" into main

parents 798d947a 70e13569
Loading
Loading
Loading
Loading
+11 −5
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -44,6 +45,7 @@ import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxOfOrDefault
import androidx.compose.ui.util.fastSumBy
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
@@ -148,12 +150,16 @@ private fun ContentScope.BundleHeaderContent(
            modifier = Modifier.element(BundleHeader.Elements.TitleText).weight(1f),
        )

        if (collapsed && viewModel.previewIcons.isNotEmpty()) {
        if (collapsed) {
            val currentPreviewIcons by
                viewModel.previewIcons.collectAsStateWithLifecycle(initialValue = emptyList())
            if (currentPreviewIcons.isNotEmpty()) {
                BundlePreviewIcons(
                previewDrawables = viewModel.previewIcons,
                    previewDrawables = currentPreviewIcons,
                    modifier = Modifier.padding(start = 8.dp),
                )
            }
        }

        ExpansionControl(
            collapsed = collapsed,
+71 −8
Original line number Diff line number Diff line
@@ -21,21 +21,27 @@ import android.app.NotificationChannel.NEWS_ID
import android.app.NotificationChannel.PROMOTIONS_ID
import android.app.NotificationChannel.RECS_ID
import android.app.NotificationChannel.SOCIAL_MEDIA_ID
import android.os.UserHandle
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.InternalNotificationsApi
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.render.BundleBarn
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.row.data.model.AppData
import com.android.systemui.statusbar.notification.row.data.repository.TEST_BUNDLE_SPEC
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -49,6 +55,12 @@ class BundleCoordinatorTest : SysuiTestCase() {

    private lateinit var coordinator: BundleCoordinator

    private val pkg1 = "pkg1"
    private val pkg2 = "pkg2"

    private val user1 = UserHandle.of(0)
    private val user2 = UserHandle.of(1)

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
@@ -58,7 +70,7 @@ class BundleCoordinatorTest : SysuiTestCase() {
                socialController,
                recsController,
                promoController,
                bundleBarn
                bundleBarn,
            )
    }

@@ -87,8 +99,8 @@ class BundleCoordinatorTest : SysuiTestCase() {
    fun promoSectioner() {
        assertThat(coordinator.promoSectioner.isInSection(makeEntryOfChannelType(PROMOTIONS_ID)))
            .isTrue()
        assertThat(coordinator.promoSectioner.isInSection(makeEntryOfChannelType("promo"))).
        isFalse()
        assertThat(coordinator.promoSectioner.isInSection(makeEntryOfChannelType("promo")))
            .isFalse()
    }

    @Test
@@ -103,16 +115,67 @@ class BundleCoordinatorTest : SysuiTestCase() {
        assertEquals(coordinator.bundler.getBundleIdOrNull(unclassifiedEntry), null)
    }

    @Test
    fun testUpdateAppData_emptyChildren_setsEmptyAppList() {
        val bundle = BundleEntry(TEST_BUNDLE_SPEC)
        assertThat(bundle.children).isEmpty()

        coordinator.bundleAppDataUpdater.onBeforeRenderList(listOf(bundle))

        assertThat(bundle.bundleRepository.appDataList).isEmpty()
    }

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

        val notif1 = NotificationEntryBuilder().setPkg(pkg1).setUser(user1).build()
        val notif2 = NotificationEntryBuilder().setPkg(pkg2).setUser(user2).build()

        bundle.addChild(notif1)
        bundle.addChild(notif2)

        coordinator.bundleAppDataUpdater.onBeforeRenderList(listOf(bundle))

        assertThat(bundle.bundleRepository.appDataList)
            .containsExactly(AppData(pkg1, user1), AppData(pkg2, user2))
    }

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

        val notif1 = NotificationEntryBuilder().setPkg(pkg1).setUser(user1).build()
        val group1 = GroupEntry("key", 0L)
        val groupChild = NotificationEntryBuilder().setPkg(pkg2).setUser(user2).build()
        group1.rawChildren.add(groupChild)
        val groupSummary =
            NotificationEntryBuilder()
                .setPkg(pkg2)
                .setUser(user2)
                .setGroupSummary(context, true)
                .build()
        group1.summary = groupSummary

        bundle.addChild(notif1)
        bundle.addChild(group1)

        coordinator.bundleAppDataUpdater.onBeforeRenderList(listOf(bundle))

        assertThat(bundle.bundleRepository.appDataList)
            .containsExactly(AppData(pkg1, user1), AppData(pkg2, user2))
    }

    private fun makeEntryOfChannelType(
        type: String,
        buildBlock: NotificationEntryBuilder.() -> Unit = {}
        buildBlock: NotificationEntryBuilder.() -> Unit = {},
    ): NotificationEntry {
        val channel: NotificationChannel = NotificationChannel(type, type, 2)
        val entry =
            NotificationEntryBuilder()
                .updateRanking {
                    it.setChannel(channel)
                }
                .updateRanking { it.setChannel(channel) }
                .also(buildBlock)
                .build()
        return entry
+11 −3
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.collection;
import android.annotation.NonNull;
import android.annotation.Nullable;

import androidx.annotation.VisibleForTesting;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -36,7 +38,8 @@ public class GroupEntry extends ListEntry {
            Collections.unmodifiableList(mChildren);
    private int mUntruncatedChildCount;

    GroupEntry(String key, long creationTime) {
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public GroupEntry(String key, long creationTime) {
        super(key, creationTime);
    }

@@ -55,7 +58,8 @@ public class GroupEntry extends ListEntry {
        return mUnmodifiableChildren;
    }

    void setSummary(@Nullable NotificationEntry summary) {
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public void setSummary(@Nullable NotificationEntry summary) {
        mSummary = summary;
    }

@@ -71,7 +75,11 @@ public class GroupEntry extends ListEntry {
        mChildren.sort(c);
    }

    List<NotificationEntry> getRawChildren() {
    /**
     * @return modifiable list of NotificationEntry children
     */
    @VisibleForTesting
    public List<NotificationEntry> getRawChildren() {
        return mChildren;
    }

+54 −12
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.app.NotificationChannel.RECS_ID
import android.app.NotificationChannel.SOCIAL_MEDIA_ID
import android.os.Build
import android.os.SystemProperties
import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.BundleSpec
import com.android.systemui.statusbar.notification.collection.GroupEntry
@@ -40,6 +41,7 @@ import com.android.systemui.statusbar.notification.dagger.NewsHeader
import com.android.systemui.statusbar.notification.dagger.PromoHeader
import com.android.systemui.statusbar.notification.dagger.RecsHeader
import com.android.systemui.statusbar.notification.dagger.SocialHeader
import com.android.systemui.statusbar.notification.row.data.model.AppData
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.BUCKET_NEWS
import com.android.systemui.statusbar.notification.stack.BUCKET_PROMO
@@ -65,7 +67,7 @@ constructor(
                return entry.asListEntry()?.representativeEntry?.channel?.id == NEWS_ID
            }

            override fun getHeaderNodeController(): NodeController? {
            override fun getHeaderNodeController(): NodeController {
                return newsHeaderController
            }
        }
@@ -76,7 +78,7 @@ constructor(
                return entry.asListEntry()?.representativeEntry?.channel?.id == SOCIAL_MEDIA_ID
            }

            override fun getHeaderNodeController(): NodeController? {
            override fun getHeaderNodeController(): NodeController {
                return socialHeaderController
            }
        }
@@ -87,7 +89,7 @@ constructor(
                return entry.asListEntry()?.representativeEntry?.channel?.id == RECS_ID
            }

            override fun getHeaderNodeController(): NodeController? {
            override fun getHeaderNodeController(): NodeController {
                return recsHeaderController
            }
        }
@@ -98,14 +100,13 @@ constructor(
                return entry.asListEntry()?.representativeEntry?.channel?.id == PROMOTIONS_ID
            }

            override fun getHeaderNodeController(): NodeController? {
            override fun getHeaderNodeController(): NodeController {
                return promoHeaderController
            }
        }

    val bundler =
        object : NotifBundler("NotifBundler") {

            // Use list instead of set to keep fixed order
            override val bundleSpecs: List<BundleSpec> = buildList {
                add(BundleSpec.NEWS)
@@ -145,20 +146,22 @@ constructor(
        }

    /** Recursively check parents until finding bundle or null */
    private fun PipelineEntry.getBundleOrNull(): BundleEntry? {
        return when (this) {
    private fun PipelineEntry.getBundleOrNull(): BundleEntry? =
        when (this) {
            is BundleEntry -> this
            is ListEntry -> parent?.getBundleOrNull()
        }
    }

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

    private val bundleFilter: NotifFilter =
        object : NotifFilter("BundleInflateFilter") {

            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
                // TODO(b/399736937) Do not hide notifications if we have a bug that means the
                //  bundle isn't inflated yet. It's better that we just show those notifications in
@@ -175,7 +178,45 @@ constructor(
        }

    private val bundleCountUpdater = OnBeforeRenderListListener { entries ->
        entries.filterIsInstance<BundleEntry>().forEach(BundleEntry::updateTotalCount)
        for (entry in entries) {
            if (entry is BundleEntry) {
                entry.updateTotalCount()
            }
        }
    }

    /**
     * For each BundleEntry, populate its bundleRepository.appDataList with AppData (package name,
     * UserHandle) from its notif children
     */
    @get:VisibleForTesting
    val bundleAppDataUpdater = OnBeforeRenderListListener { entries ->
        for (entry in entries) {
            if (entry !is BundleEntry) continue
            val newAppDataList: List<AppData> =
                entry.children.flatMap { listEntry ->
                    when (listEntry) {
                        is NotificationEntry -> {
                            listOf(AppData(listEntry.sbn.packageName, listEntry.sbn.user))
                        }

                        is GroupEntry -> {
                            val summary = listEntry.representativeEntry
                            if (summary != null) {
                                listOf(AppData(summary.sbn.packageName, summary.sbn.user))
                            } else {
                                error(
                                    "BundleEntry (key: ${entry.key}) contains GroupEntry " +
                                        "(key: ${listEntry.key}) with no summary."
                                )
                            }
                        }

                        else -> error("Unexpected ListEntry type: ${listEntry::class.simpleName}")
                    }
                }
            entry.bundleRepository.appDataList = newAppDataList
        }
    }

    override fun attach(pipeline: NotifPipeline) {
@@ -184,6 +225,7 @@ constructor(
            pipeline.addOnBeforeFinalizeFilterListener(this::inflateAllBundleEntries)
            pipeline.addFinalizeFilter(bundleFilter)
            pipeline.addOnBeforeRenderListListener(bundleCountUpdater)
            pipeline.addOnBeforeRenderListListener(bundleAppDataUpdater)
        }
    }

+31 −20
Original line number Diff line number Diff line
@@ -19,7 +19,9 @@ package com.android.systemui.statusbar.notification.collection.render
import android.content.Context
import android.view.ViewGroup
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.PipelineDumpable
@@ -30,16 +32,16 @@ import com.android.systemui.statusbar.notification.row.RowInflaterTask
import com.android.systemui.statusbar.notification.row.RowInflaterTaskLogger
import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent
import com.android.systemui.statusbar.notification.row.domain.interactor.BundleInteractor
import com.android.systemui.statusbar.notification.row.icon.AppIconProvider
import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModel
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.util.time.SystemClock
import dagger.Lazy
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher

/**
 * Class that handles inflating BundleEntry view and controller, for use by NodeSpecBuilder.
 */
/** Class that handles inflating BundleEntry view and controller, for use by NodeSpecBuilder. */
@SysUISingleton
class BundleBarn
@Inject
@@ -47,18 +49,18 @@ constructor(
    private val rowComponent: ExpandableNotificationRowComponent.Builder,
    private val rowInflaterTaskProvider: Provider<RowInflaterTask>,
    private val listContainer: NotificationListContainer,
    val context: Context? = null,
    @ShadeDisplayAware val context: Context,
    val systemClock: SystemClock,
    val logger: RowInflaterTaskLogger,
    val userTracker: UserTracker,
    private val presenterLazy: Lazy<NotificationPresenter?>? = null,
    private val appIconProvider: AppIconProvider,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : PipelineDumpable {

    /**
     * Map of [BundleEntry] key to [NodeController]:
     *     no key -> not started
     *     key maps to null -> inflating
     *     key maps to controller -> inflated
     * Map of [BundleEntry] key to [NodeController]: no key -> not started key maps to null ->
     * inflating key maps to controller -> inflated
     */
    private val keyToControllerMap = mutableMapOf<String, NotifViewController?>()

@@ -86,9 +88,14 @@ constructor(
            keyToControllerMap[bundleEntry.key] = controller

            // TODO(389839492): Construct BundleHeaderViewModel (or even ENRViewModel) by dagger
            row.initBundleHeader(
                BundleHeaderViewModel(BundleInteractor(bundleEntry.bundleRepository))
            val bundleInteractor =
                BundleInteractor(
                    repository = bundleEntry.bundleRepository,
                    appIconProvider = appIconProvider,
                    context = context,
                    backgroundDispatcher = backgroundDispatcher,
                )
            row.initBundleHeader(BundleHeaderViewModel(bundleInteractor))
        }
        debugBundleLog(TAG, { "calling inflate: ${bundleEntry.key}" })
        keyToControllerMap[bundleEntry.key] = null
@@ -104,10 +111,13 @@ constructor(

    /** Return ExpandableNotificationRowController for BundleEntry. */
    fun requireNodeController(bundleEntry: BundleEntry): NodeController {
        debugBundleLog(TAG, {
        debugBundleLog(
            TAG,
            {
                "requireNodeController: ${bundleEntry.key}" +
                    "controller: ${keyToControllerMap[bundleEntry.key]}"
        })
            },
        )
        return keyToControllerMap[bundleEntry.key]
            ?: error("No view has been registered for bundle: ${bundleEntry.key}")
    }
@@ -119,7 +129,8 @@ constructor(
        } else {
            d.println("Bundle Inflation States:")
            keyToControllerMap.forEach { (key, controller) ->
                val stateString = if (controller == null) {
                val stateString =
                    if (controller == null) {
                        "INFLATING"
                    } else {
                        "INFLATED (Controller: ${controller::class.simpleName})"
Loading