summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt70
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt26
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt130
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt90
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt18
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt10
9 files changed, 403 insertions, 34 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
new file mode 100644
index 000000000000..19ed6a57d2f0
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.chips.notification.domain.interactor
+
+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.coroutines.collectValues
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(StatusBarNotifChips.FLAG_NAME)
+class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testScope = kosmos.testScope
+
+ private val underTest = kosmos.statusBarNotificationChipsInteractor
+
+ @Test
+ fun onPromotedNotificationChipTapped_emitsKeys() =
+ testScope.runTest {
+ val latest by collectValues(underTest.promotedNotificationChipTapEvent)
+
+ underTest.onPromotedNotificationChipTapped("fakeKey")
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest[0]).isEqualTo("fakeKey")
+
+ underTest.onPromotedNotificationChipTapped("fakeKey2")
+
+ assertThat(latest).hasSize(2)
+ assertThat(latest[1]).isEqualTo("fakeKey2")
+ }
+
+ @Test
+ fun onPromotedNotificationChipTapped_sameKeyTwice_emitsTwice() =
+ testScope.runTest {
+ val latest by collectValues(underTest.promotedNotificationChipTapEvent)
+
+ underTest.onPromotedNotificationChipTapped("fakeKey")
+ underTest.onPromotedNotificationChipTapped("fakeKey")
+
+ assertThat(latest).hasSize(2)
+ assertThat(latest[0]).isEqualTo("fakeKey")
+ assertThat(latest[1]).isEqualTo("fakeKey")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index eb5d9318c88f..6e190965d08b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -17,12 +17,14 @@
package com.android.systemui.statusbar.chips.notification.ui.viewmodel
import android.platform.test.annotations.EnableFlags
+import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
@@ -102,6 +104,30 @@ class NotifChipsViewModelTest : SysuiTestCase() {
assertIsNotifChip(latest!![1], secondIcon)
}
+ @Test
+ fun chips_clickingChipNotifiesInteractor() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chips)
+ val latestChipTap by
+ collectLastValue(
+ kosmos.statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent
+ )
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "clickTest",
+ statusBarChipIcon = mock<StatusBarIconView>(),
+ )
+ )
+ )
+ val chip = latest!![0]
+
+ chip.onClickListener!!.onClick(mock<View>())
+
+ assertThat(latestChipTap).isEqualTo("clickTest")
+ }
+
private fun setNotifs(notifs: List<ActiveNotificationModel>) {
activeNotificationListRepository.activeNotifications.value =
ActiveNotificationsStore.Builder()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 76bb8de71bdd..9613f76c2b48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -17,12 +17,18 @@ package com.android.systemui.statusbar.notification.collection.coordinator
import android.app.Notification.GROUP_ALERT_ALL
import android.app.Notification.GROUP_ALERT_SUMMARY
+import android.platform.test.annotations.EnableFlags
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.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.notification.HeadsUpManagerPhone
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -32,6 +38,8 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.mockNotifCollection
+import com.android.systemui.statusbar.notification.collection.notifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
@@ -43,9 +51,9 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback
-import com.android.systemui.statusbar.notification.HeadsUpManagerPhone
import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -54,6 +62,7 @@ import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
import java.util.ArrayList
import java.util.function.Consumer
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -73,6 +82,11 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class)
@RunWithLooper
class HeadsUpCoordinatorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val statusBarNotificationChipsInteractor = kosmos.statusBarNotificationChipsInteractor
+ private val notifCollection = kosmos.mockNotifCollection
+
private lateinit var coordinator: HeadsUpCoordinator
// captured listeners and pluggables:
@@ -115,16 +129,19 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
helper = NotificationGroupTestHelper(mContext)
coordinator =
HeadsUpCoordinator(
+ kosmos.applicationCoroutineScope,
logger,
systemClock,
+ notifCollection,
headsUpManager,
headsUpViewBinder,
visualInterruptionDecisionProvider,
remoteInputManager,
launchFullScreenIntentProvider,
flags,
+ statusBarNotificationChipsInteractor,
headerController,
- executor
+ executor,
)
coordinator.attach(notifPipeline)
@@ -351,7 +368,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
assertFalse(
notifLifetimeExtender.maybeExtendLifetime(
NotificationEntryBuilder().setPkg("test-package").build(),
- /* reason= */ 0
+ /* reason= */ 0,
)
)
}
@@ -442,6 +459,97 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun showPromotedNotification_hasNotifEntry_shownAsHUN() =
+ testScope.runTest {
+ whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
+
+ statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key)
+ executor.advanceClockToLast()
+ executor.runAllReady()
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+ finishBind(entry)
+ verify(headsUpManager).showNotification(entry)
+ }
+
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun showPromotedNotification_noNotifEntry_noHUN() =
+ testScope.runTest {
+ whenever(notifCollection.getEntry(entry.key)).thenReturn(null)
+
+ statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key)
+ executor.advanceClockToLast()
+ executor.runAllReady()
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+ verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any())
+ verify(headsUpManager, never()).showNotification(entry)
+ }
+
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() =
+ testScope.runTest {
+ whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
+
+ // First, add the entry as shouldn't HUN
+ setShouldHeadsUp(entry, false)
+ collectionListener.onEntryAdded(entry)
+ beforeTransformGroupsListener.onBeforeTransformGroups(listOf(entry))
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+ // WHEN that entry becomes a promoted notification and is tapped
+ statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key)
+ executor.advanceClockToLast()
+ executor.runAllReady()
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+ // THEN it's still shown as heads up
+ finishBind(entry)
+ verify(headsUpManager).showNotification(entry)
+ }
+
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() =
+ testScope.runTest {
+ // First, the promoted notification appears as not heads up
+ val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build()
+ whenever(notifCollection.getEntry(promotedEntry.key)).thenReturn(promotedEntry)
+ setShouldHeadsUp(promotedEntry, false)
+
+ collectionListener.onEntryAdded(promotedEntry)
+ beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry))
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry))
+
+ verify(headsUpViewBinder, never()).bindHeadsUpView(eq(promotedEntry), any())
+ verify(headsUpManager, never()).showNotification(promotedEntry)
+
+ // Then a new notification comes in that should be heads up
+ setShouldHeadsUp(entry, false)
+ whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
+ collectionListener.onEntryAdded(entry)
+
+ // At the same time, the promoted notification chip is tapped
+ statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(promotedEntry.key)
+ executor.advanceClockToLast()
+ executor.runAllReady()
+
+ // WHEN we finalize the pipeline
+ beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry, entry))
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry, entry))
+
+ // THEN the promoted entry is shown as a HUN, *not* the new entry
+ finishBind(promotedEntry)
+ verify(headsUpManager).showNotification(promotedEntry)
+
+ verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any())
+ verify(headsUpManager, never()).showNotification(entry)
+ }
+
+ @Test
fun testTransferIsolatedChildAlert_withGroupAlertSummary() {
setShouldHeadsUp(groupSummary)
whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1))
@@ -862,7 +970,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(launchFullScreenIntentProvider).launchFullScreenIntent(entry)
verifyLoggedFullScreenIntentDecision(
entry,
- FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE
+ FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE,
)
}
@@ -885,7 +993,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
verifyLoggedFullScreenIntentDecision(
entry,
- FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND
+ FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND,
)
}
@@ -899,7 +1007,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
verifyLoggedFullScreenIntentDecision(
entry,
- FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND
+ FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND,
)
clearInterruptionProviderInvocations()
@@ -917,7 +1025,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(headsUpManager, never()).showNotification(any())
verifyLoggedFullScreenIntentDecision(
entry,
- FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE
+ FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE,
)
clearInterruptionProviderInvocations()
@@ -942,7 +1050,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
verifyLoggedFullScreenIntentDecision(
entry,
- FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND
+ FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND,
)
clearInterruptionProviderInvocations()
@@ -975,7 +1083,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(headsUpManager, never()).showNotification(any())
verifyLoggedFullScreenIntentDecision(
entry,
- FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE
+ FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE,
)
clearInterruptionProviderInvocations()
}
@@ -1070,7 +1178,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
private fun setShouldFullScreen(
entry: NotificationEntry,
- originalDecision: FullScreenIntentDecision
+ originalDecision: FullScreenIntentDecision,
) {
whenever(visualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry))
.thenAnswer { FullScreenIntentDecisionImpl(entry, originalDecision) }
@@ -1078,7 +1186,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
private fun verifyLoggedFullScreenIntentDecision(
entry: NotificationEntry,
- originalDecision: FullScreenIntentDecision
+ originalDecision: FullScreenIntentDecision,
) {
val decision = withArgCaptor {
verify(visualInterruptionDecisionProvider).logFullScreenIntentDecision(capture())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
new file mode 100644
index 000000000000..9e09671bc7bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.chips.notification.domain.interactor
+
+import android.annotation.SuppressLint
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+/** An interactor for the notification chips shown in the status bar. */
+@SysUISingleton
+class StatusBarNotificationChipsInteractor @Inject constructor() {
+
+ // Each chip tap is an individual event, *not* a state, which is why we're using SharedFlow not
+ // StateFlow. There shouldn't be multiple updates per frame, which should avoid performance
+ // problems.
+ @SuppressLint("SharedFlowCreation")
+ private val _promotedNotificationChipTapEvent = MutableSharedFlow<String>()
+
+ /**
+ * SharedFlow that emits each time a promoted notification's status bar chip is tapped. The
+ * emitted value is the promoted notification's key.
+ */
+ val promotedNotificationChipTapEvent: SharedFlow<String> =
+ _promotedNotificationChipTapEvent.asSharedFlow()
+
+ suspend fun onPromotedNotificationChipTapped(key: String) {
+ StatusBarNotifChips.assertInNewMode()
+ _promotedNotificationChipTapEvent.emit(key)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index 6ae92637bde7..c8d3f339b3e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -16,20 +16,30 @@
package com.android.systemui.statusbar.chips.notification.ui.viewmodel
+import android.view.View
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
/** A view model for status bar chips for promoted ongoing notifications. */
@SysUISingleton
class NotifChipsViewModel
@Inject
-constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) {
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ private val notifChipsInteractor: StatusBarNotificationChipsInteractor,
+) {
/**
* A flow modeling the notification chips that should be shown. Emits an empty list if there are
* no notifications that should show a status bar chip.
@@ -44,13 +54,20 @@ constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) {
* notification has invalid data such that it can't be displayed as a chip.
*/
private fun ActiveNotificationModel.toChipModel(): OngoingActivityChipModel.Shown? {
+ StatusBarNotifChips.assertInNewMode()
// TODO(b/364653005): Log error if there's no icon view.
val rawIcon = this.statusBarChipIconView ?: return null
val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(rawIcon)
// TODO(b/364653005): Use the notification color if applicable.
val colors = ColorsModel.Themed
- // TODO(b/364653005): When the chip is clicked, show the HUN.
- val onClickListener = null
+ val onClickListener =
+ View.OnClickListener {
+ // The notification pipeline needs everything to run on the main thread, so keep
+ // this event on the main thread.
+ applicationScope.launch {
+ notifChipsInteractor.onPromotedNotificationChipTapped(this@toChipModel.key)
+ }
+ }
return OngoingActivityChipModel.Shown.ShortTimeDelta(
icon,
colors,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index ec8566b82aea..c7b47eeec218 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -20,11 +20,15 @@ import android.app.Notification.GROUP_ALERT_SUMMARY
import android.util.ArrayMap
import android.util.ArraySet
import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
+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.coordinator.dagger.CoordinatorScope
@@ -48,6 +52,8 @@ import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
import java.util.function.Consumer
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
/**
* Coordinates heads up notification (HUN) interactions with the notification pipeline based on the
@@ -67,16 +73,19 @@ import javax.inject.Inject
class HeadsUpCoordinator
@Inject
constructor(
+ @Application private val applicationScope: CoroutineScope,
private val mLogger: HeadsUpCoordinatorLogger,
private val mSystemClock: SystemClock,
+ private val notifCollection: NotifCollection,
private val mHeadsUpManager: HeadsUpManager,
private val mHeadsUpViewBinder: HeadsUpViewBinder,
private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider,
private val mRemoteInputManager: NotificationRemoteInputManager,
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
private val mFlags: NotifPipelineFlags,
+ private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor,
@IncomingHeader private val mIncomingHeaderController: NodeController,
- @Main private val mExecutor: DelayableExecutor
+ @Main private val mExecutor: DelayableExecutor,
) : Coordinator {
private val mEntriesBindingUntil = ArrayMap<String, Long>()
private val mEntriesUpdateTimes = ArrayMap<String, Long>()
@@ -98,6 +107,52 @@ constructor(
pipeline.addPromoter(mNotifPromoter)
pipeline.addNotificationLifetimeExtender(mLifetimeExtender)
mRemoteInputManager.addActionPressListener(mActionPressListener)
+
+ if (StatusBarNotifChips.isEnabled) {
+ applicationScope.launch {
+ statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect {
+ showPromotedNotificationHeadsUp(it)
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows the promoted notification with the given [key] as heads-up.
+ *
+ * Must be run on the main thread.
+ */
+ private fun showPromotedNotificationHeadsUp(key: String) {
+ StatusBarNotifChips.assertInNewMode()
+ mLogger.logShowPromotedNotificationHeadsUp(key)
+
+ val entry = notifCollection.getEntry(key)
+ if (entry == null) {
+ mLogger.logPromotedNotificationForHeadsUpNotFound(key)
+ return
+ }
+ // TODO(b/364653005): Validate that the given key indeed matches a promoted notification,
+ // not just any notification.
+
+ val posted =
+ PostedEntry(
+ entry,
+ wasAdded = false,
+ wasUpdated = false,
+ // Force-set this notification to show heads-up.
+ // TODO(b/364653005): This means that if you tap on the second notification chip,
+ // then it moves to become the first chip because whatever notification is showing
+ // heads-up is considered to be the top notification.
+ shouldHeadsUpEver = true,
+ shouldHeadsUpAgain = true,
+ isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key),
+ isBinding = isEntryBinding(entry),
+ )
+
+ mExecutor.execute {
+ mPostedEntries[entry.key] = posted
+ mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}")
+ }
}
private fun onHeadsUpViewBound(entry: NotificationEntry) {
@@ -222,7 +277,7 @@ constructor(
logicalSummary.setInterruption()
mLogger.logSummaryMarkedInterrupted(
logicalSummary.key,
- childToReceiveParentHeadsUp.key
+ childToReceiveParentHeadsUp.key,
)
// If the summary was not attached, then remove the heads up from the detached
@@ -246,12 +301,12 @@ constructor(
handlePostedEntry(
summaryUpdateForRemoval,
hunMutator,
- scenario = "detached-summary-remove-heads-up"
+ scenario = "detached-summary-remove-heads-up",
)
} else if (summaryUpdate != null) {
mLogger.logPostedEntryWillNotEvaluate(
summaryUpdate,
- reason = "attached-summary-transferred"
+ reason = "attached-summary-transferred",
)
}
@@ -270,14 +325,14 @@ constructor(
handlePostedEntry(
postedEntry,
hunMutator,
- scenario = "child-heads-up-transfer-target-$targetType"
+ scenario = "child-heads-up-transfer-target-$targetType",
)
didHeadsUpChildToReceiveParentHeadsUp = true
} else {
handlePostedEntry(
postedEntry,
hunMutator,
- scenario = "child-heads-up-non-target"
+ scenario = "child-heads-up-non-target",
)
}
}
@@ -301,7 +356,7 @@ constructor(
handlePostedEntry(
posted,
hunMutator,
- scenario = "non-posted-child-heads-up-transfer-target-$targetType"
+ scenario = "non-posted-child-heads-up-transfer-target-$targetType",
)
}
}
@@ -345,10 +400,7 @@ constructor(
.filter { !it.sbn.notification.isGroupSummary }
.filter { locationLookupByKey(it.key) != GroupLocation.Detached }
.sortedWith(
- compareBy(
- { !mPostedEntries.contains(it.key) },
- { -it.sbn.notification.getWhen() },
- )
+ compareBy({ !mPostedEntries.contains(it.key) }, { -it.sbn.notification.getWhen() })
)
.firstOrNull()
@@ -499,7 +551,7 @@ constructor(
mHeadsUpManager.removeNotification(
posted.key,
/* removeImmediately= */ false,
- "onEntryUpdated"
+ "onEntryUpdated",
)
} else if (posted.isBinding) {
// Don't let the bind finish
@@ -527,7 +579,7 @@ constructor(
mHeadsUpManager.removeNotification(
entry.key,
removeImmediatelyForRemoteInput,
- "onEntryRemoved, reason: $reason"
+ "onEntryRemoved, reason: $reason",
)
}
}
@@ -593,7 +645,7 @@ constructor(
// for FSI reconsideration
mLogger.logEntryDisqualifiedFromFullScreen(
entry.key,
- decision.logReason
+ decision.logReason,
)
mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(
decision
@@ -619,7 +671,7 @@ constructor(
mLogger.logEntryUpdatedByRanking(
entry.key,
shouldHeadsUpEver,
- decision.logReason
+ decision.logReason,
)
onEntryUpdated(entry)
}
@@ -731,10 +783,10 @@ constructor(
entry.key, /* releaseImmediately */
true,
"cancel lifetime extension - extended for reason: " +
- "$reason, isSticky: true"
+ "$reason, isSticky: true",
)
},
- removeAfterMillis
+ removeAfterMillis,
)
} else {
mExecutor.execute {
@@ -742,7 +794,7 @@ constructor(
entry.key, /* releaseImmediately */
false,
"lifetime extension - extended for reason: $reason" +
- ", isSticky: false"
+ ", isSticky: false",
)
}
mNotifsExtendingLifetime[entry] = null
@@ -873,7 +925,7 @@ private enum class GroupLocation {
Detached,
Isolated,
Summary,
- Child
+ Child,
}
private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 1a521d767438..e443a0418ffd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -138,4 +138,22 @@ class HeadsUpCoordinatorLogger(private val buffer: LogBuffer, private val verbos
{ "marked group summary as interrupted: $str1 for alert transfer to child: $str2" },
)
}
+
+ fun logShowPromotedNotificationHeadsUp(key: String) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { str1 = key },
+ { "requesting promoted entry to show heads up: $str1" },
+ )
+ }
+
+ fun logPromotedNotificationForHeadsUpNotFound(key: String) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { str1 = key },
+ { "could not find promoted entry, so not showing heads up: $str1" },
+ )
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt
new file mode 100644
index 000000000000..74c7611a6392
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.chips.notification.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor by
+ Kosmos.Fixture { StatusBarNotificationChipsInteractor() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
index af24c371d62b..68b28adb4b3a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
@@ -17,7 +17,15 @@
package com.android.systemui.statusbar.chips.notification.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
val Kosmos.notifChipsViewModel: NotifChipsViewModel by
- Kosmos.Fixture { NotifChipsViewModel(activeNotificationsInteractor) }
+ Kosmos.Fixture {
+ NotifChipsViewModel(
+ applicationCoroutineScope,
+ activeNotificationsInteractor,
+ statusBarNotificationChipsInteractor,
+ )
+ }