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

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

Merge "Show status bar notification icons on all displays" into main

parents 59c6d2a1 b96a6f9c
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -53,7 +53,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
    private val shadeRootview = mock<WindowRootView>()
    private val positionRepository = FakeShadeDisplayRepository()
    private val shadeContext = mock<Context>()
    private val contextStore = FakeDisplayWindowPropertiesRepository()
    private val contextStore = FakeDisplayWindowPropertiesRepository(context)
    private val testScope = TestScope(UnconfinedTestDispatcher())
    private val shadeWm = mock<WindowManager>()
    private val resources = mock<Resources>()
+149 −0
Original line number Diff line number Diff line
/*
 * 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.notification.icon.ui.viewbinder

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.domain.interactor.displayWindowPropertiesInteractor
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.statusbar.RankingBuilder
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.notifCollection
import com.android.systemui.statusbar.notification.collection.notifPipeline
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.icon.iconManager
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
class ConnectedDisplaysStatusBarNotificationIconViewStoreTest : SysuiTestCase() {

    private val kosmos = testKosmos()

    private val underTest =
        ConnectedDisplaysStatusBarNotificationIconViewStore(
            TEST_DISPLAY_ID,
            kosmos.notifCollection,
            kosmos.iconManager,
            kosmos.displayWindowPropertiesInteractor,
            kosmos.notifPipeline,
        )

    private val notifCollectionListeners = mutableListOf<NotifCollectionListener>()

    @Before
    fun setupNoticCollectionListener() {
        whenever(kosmos.notifPipeline.addCollectionListener(any())).thenAnswer { invocation ->
            notifCollectionListeners.add(invocation.arguments[0] as NotifCollectionListener)
        }
    }

    @Before
    fun activate() {
        underTest.activateIn(kosmos.testScope)
    }

    @Test
    fun iconView_unknownKey_returnsNull() =
        kosmos.testScope.runTest {
            val unknownKey = "unknown key"

            assertThat(underTest.iconView(unknownKey)).isNull()
        }

    @Test
    fun iconView_knownKey_returnsNonNull() =
        kosmos.testScope.runTest {
            val entry = createEntry()

            whenever(kosmos.notifCollection.getEntry(entry.key)).thenReturn(entry)

            assertThat(underTest.iconView(entry.key)).isNotNull()
        }

    @Test
    fun iconView_knownKey_calledMultipleTimes_returnsSameInstance() =
        kosmos.testScope.runTest {
            val entry = createEntry()

            whenever(kosmos.notifCollection.getEntry(entry.key)).thenReturn(entry)

            val first = underTest.iconView(entry.key)
            val second = underTest.iconView(entry.key)

            assertThat(first).isSameInstanceAs(second)
        }

    @Test
    fun iconView_knownKey_afterNotificationRemoved_returnsNewInstance() =
        kosmos.testScope.runTest {
            val entry = createEntry()

            whenever(kosmos.notifCollection.getEntry(entry.key)).thenReturn(entry)

            val first = underTest.iconView(entry.key)

            notifCollectionListeners.forEach { it.onEntryRemoved(entry, /* reason= */ 0) }

            val second = underTest.iconView(entry.key)

            assertThat(first).isNotSameInstanceAs(second)
        }

    private fun createEntry(): NotificationEntry {
        val channelId = "channelId"
        val notificationChannel =
            NotificationChannel(channelId, "name", NotificationManager.IMPORTANCE_DEFAULT)
        val notification =
            Notification.Builder(context, channelId)
                .setContentTitle("Title")
                .setContentText("Content text")
                .setSmallIcon(com.android.systemui.res.R.drawable.icon)
                .build()
        val statusBarNotification = SbnBuilder().setNotification(notification).build()
        val ranking =
            RankingBuilder()
                .setChannel(notificationChannel)
                .setKey(statusBarNotification.key)
                .build()
        return NotificationEntry(
            /* sbn = */ statusBarNotification,
            /* ranking = */ ranking,
            /* creationTime = */ 1234L,
        )
    }

    private companion object {
        const val TEST_DISPLAY_ID = 1234
    }
}
+5 −1
Original line number Diff line number Diff line
@@ -25,7 +25,11 @@ import javax.inject.Inject

/** Testable wrapper around Context. */
class IconBuilder @Inject constructor(private val context: Context) {
    fun createIconView(entry: NotificationEntry): StatusBarIconView {
    @JvmOverloads
    fun createIconView(
        entry: NotificationEntry,
        context: Context = this.context,
    ): StatusBarIconView {
        return StatusBarIconView(
            context,
            "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}",
+54 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.icon
import android.app.Notification
import android.app.Notification.MessagingStyle
import android.app.Person
import android.content.Context
import android.content.pm.LauncherApps
import android.graphics.drawable.Icon
import android.os.Build
@@ -36,6 +37,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -68,6 +70,17 @@ constructor(
    @Background private val bgCoroutineContext: CoroutineContext,
    @Main private val mainCoroutineContext: CoroutineContext,
) : ConversationIconManager {

    /**
     * A listener that is notified when a [NotificationEntry] has been updated and the associated
     * icons have to be updated as well.
     */
    fun interface OnIconUpdateRequiredListener {
        fun onIconUpdateRequired(entry: NotificationEntry)
    }

    private val onIconUpdateRequiredListeners = mutableSetOf<OnIconUpdateRequiredListener>()

    private var unimportantConversationKeys: Set<String> = emptySet()
    /**
     * A map of running jobs for fetching the person avatar from launcher. The key is the
@@ -76,6 +89,16 @@ constructor(
    private var launcherPeopleAvatarIconJobs: ConcurrentHashMap<String, Job> =
        ConcurrentHashMap<String, Job>()

    fun addIconsUpdateListener(listener: OnIconUpdateRequiredListener) {
        StatusBarConnectedDisplays.assertInNewMode()
        onIconUpdateRequiredListeners += listener
    }

    fun removeIconsUpdateListener(listener: OnIconUpdateRequiredListener) {
        StatusBarConnectedDisplays.assertInNewMode()
        onIconUpdateRequiredListeners -= listener
    }

    fun attach() {
        notifCollection.addCollectionListener(entryListener)
    }
@@ -111,6 +134,21 @@ constructor(
        }
    }

    /**
     * Inflate the [StatusBarIconView] for the given [NotificationEntry], using the specified
     * [Context].
     */
    fun createSbIconView(context: Context, entry: NotificationEntry): StatusBarIconView =
        traceSection("IconManager.createSbIconView") {
            StatusBarConnectedDisplays.assertInNewMode()

            val sbIcon = iconBuilder.createIconView(entry, context)
            sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
            val (normalIconDescriptor, _) = getIconDescriptors(entry)
            setIcon(entry, normalIconDescriptor, sbIcon)
            return sbIcon
        }

    /**
     * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the
     * result in [NotificationEntry.getIcons].
@@ -159,6 +197,18 @@ constructor(
            }
        }

    /** Update the [StatusBarIconView] for the given [NotificationEntry]. */
    fun updateSbIcon(entry: NotificationEntry, iconView: StatusBarIconView) =
        traceSection("IconManager.updateSbIcon") {
            StatusBarConnectedDisplays.assertInNewMode()

            val (normalIconDescriptor, _) = getIconDescriptors(entry)
            val notificationContentDescription =
                entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) }
            iconView.setNotification(entry.sbn, notificationContentDescription)
            setIcon(entry, normalIconDescriptor, iconView)
        }

    /**
     * Update the notification icons.
     *
@@ -172,6 +222,10 @@ constructor(
                return@traceSection
            }

            if (StatusBarConnectedDisplays.isEnabled) {
                onIconUpdateRequiredListeners.onEach { it.onIconUpdateRequired(entry) }
            }

            if (usingCache && !Flags.notificationsBackgroundIcons()) {
                Log.wtf(
                    TAG,
+94 −0
Original line number Diff line number Diff line
/*
 * 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.notification.icon.ui.viewbinder

import com.android.systemui.display.domain.interactor.DisplayWindowPropertiesInteractor
import com.android.systemui.lifecycle.Activatable
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.icon.IconManager
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope

/** [IconViewStore] for the status bar on multiple displays. */
class ConnectedDisplaysStatusBarNotificationIconViewStore
@AssistedInject
constructor(
    @Assisted private val displayId: Int,
    private val notifCollection: NotifCollection,
    private val iconManager: IconManager,
    private val displayWindowPropertiesInteractor: DisplayWindowPropertiesInteractor,
    private val notifPipeline: NotifPipeline,
) : IconViewStore, Activatable {

    private val cachedIcons = ConcurrentHashMap<String, StatusBarIconView>()

    private val iconUpdateRequiredListener =
        object : IconManager.OnIconUpdateRequiredListener {
            override fun onIconUpdateRequired(entry: NotificationEntry) {
                val iconView = iconView(entry.key) ?: return
                iconManager.updateSbIcon(entry, iconView)
            }
        }

    private val notifCollectionListener =
        object : NotifCollectionListener {
            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
                cachedIcons.remove(entry.key)
            }
        }

    override fun iconView(key: String): StatusBarIconView? {
        val entry = notifCollection.getEntry(key) ?: return null
        return cachedIcons.computeIfAbsent(key) {
            val context = displayWindowPropertiesInteractor.getForStatusBar(displayId).context
            iconManager.createSbIconView(context, entry)
        }
    }

    override suspend fun activate() = coroutineScope {
        start()
        try {
            awaitCancellation()
        } finally {
            stop()
        }
    }

    private fun start() {
        notifPipeline.addCollectionListener(notifCollectionListener)
        iconManager.addIconsUpdateListener(iconUpdateRequiredListener)
    }

    private fun stop() {
        notifPipeline.removeCollectionListener(notifCollectionListener)
        iconManager.removeIconsUpdateListener(iconUpdateRequiredListener)
    }

    @AssistedFactory
    interface Factory {
        fun create(displayId: Int): ConnectedDisplaysStatusBarNotificationIconViewStore
    }
}
Loading