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 0bf21af7026b5..0311324241158 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 @@ -15,9 +15,13 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator +import android.app.Notification +import android.app.Notification.GROUP_ALERT_SUMMARY +import android.util.ArrayMap import android.util.ArraySet import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -29,13 +33,13 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.dagger.IncomingHeader -import com.android.systemui.statusbar.notification.interruption.HeadsUpController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import javax.inject.Inject /** @@ -54,27 +58,283 @@ import javax.inject.Inject */ @CoordinatorScope class HeadsUpCoordinator @Inject constructor( + private val mLogger: HeadsUpCoordinatorLogger, + private val mSystemClock: SystemClock, private val mHeadsUpManager: HeadsUpManager, private val mHeadsUpViewBinder: HeadsUpViewBinder, private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider, private val mRemoteInputManager: NotificationRemoteInputManager, @IncomingHeader private val mIncomingHeaderController: NodeController, - @Main private val mExecutor: DelayableExecutor + @Main private val mExecutor: DelayableExecutor, ) : Coordinator { + private val mEntriesBindingUntil = ArrayMap() private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null + private lateinit var mNotifPipeline: NotifPipeline + private var mNow: Long = -1 // notifs we've extended the lifetime for private val mNotifsExtendingLifetime = ArraySet() override fun attach(pipeline: NotifPipeline) { + mNotifPipeline = pipeline mHeadsUpManager.addListener(mOnHeadsUpChangedListener) pipeline.addCollectionListener(mNotifCollectionListener) + pipeline.addOnBeforeTransformGroupsListener(::onBeforeTransformGroups) + pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter) pipeline.addPromoter(mNotifPromoter) pipeline.addNotificationLifetimeExtender(mLifetimeExtender) } private fun onHeadsUpViewBound(entry: NotificationEntry) { mHeadsUpManager.showNotification(entry) + mEntriesBindingUntil.remove(entry.key) + } + + /** + * Once the pipeline starts running, we can look through posted entries and quickly process + * any that don't have groups, and thus will never gave a group alert edge case. + */ + fun onBeforeTransformGroups(list: List) { + mNow = mSystemClock.currentTimeMillis() + if (mPostedEntries.isEmpty()) { + return + } + // Process all non-group adds/updates + mPostedEntries.values.toList().forEach { posted -> + if (!posted.entry.sbn.isGroup) { + handlePostedEntry(posted, "non-group") + mPostedEntries.remove(posted.key) + } + } + } + + /** + * Once we have a nearly final shade list (not including what's pruned for inflation reasons), + * we know that stability and [NotifPromoter]s have been applied, so we can use the location of + * notifications in this list to determine what kind of group alert behavior should happen. + */ + fun onBeforeFinalizeFilter(list: List) { + // Nothing to do if there are no other adds/updates + if (mPostedEntries.isEmpty()) { + return + } + // Calculate a bunch of information about the logical group and the locations of group + // entries in the nearly-finalized shade list. These may be used in the per-group loop. + val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey } + val logicalMembersByGroup = mNotifPipeline.allNotifs.asSequence() + .filter { postedEntriesByGroup.contains(it.sbn.groupKey) } + .groupBy { it.sbn.groupKey } + val groupLocationsByKey: Map by lazy { getGroupLocationsByKey(list) } + mLogger.logEvaluatingGroups(postedEntriesByGroup.size) + // For each group, determine which notification(s) for a group should alert. + postedEntriesByGroup.forEach { (groupKey, postedEntries) -> + // get and classify the logical members + val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList() + val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary } + + // Report the start of this group's evaluation + mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size) + + // If there is no logical summary, then there is no alert to transfer + if (logicalSummary == null) { + postedEntries.forEach { handlePostedEntry(it, "logical-summary-missing") } + return@forEach + } + + // If summary isn't wanted to be heads up, then there is no alert to transfer + if (!isGoingToShowHunStrict(logicalSummary)) { + postedEntries.forEach { handlePostedEntry(it, "logical-summary-not-alerting") } + return@forEach + } + + // The group is alerting! Overall goals: + // - Maybe transfer its alert to a child + // - Also let any/all newly alerting children still alert + var childToReceiveParentAlert: NotificationEntry? + var targetType = "undefined" + + // If the parent is alerting, always look at the posted notification with the newest + // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive the + // parent's alert. + childToReceiveParentAlert = + findAlertOverride(postedEntries, groupLocationsByKey::getLocation) + if (childToReceiveParentAlert != null) { + targetType = "alertOverride" + } + + // If the summary is Detached and we have not picked a receiver of the alert, then we + // need to look for the best child to alert in place of the summary. + val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key) + if (!isSummaryAttached && childToReceiveParentAlert == null) { + childToReceiveParentAlert = + findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation) + if (childToReceiveParentAlert != null) { + targetType = "bestChild" + } + } + + // If there is no child to receive the parent alert, then just handle the posted entries + // and return. + if (childToReceiveParentAlert == null) { + postedEntries.forEach { handlePostedEntry(it, "no-transfer-target") } + return@forEach + } + + // At this point we just need to initiate the transfer + val summaryUpdate = mPostedEntries[logicalSummary.key] + + // If the summary was not attached, then remove the alert from the detached summary. + // Otherwise we can simply ignore its posted update. + if (!isSummaryAttached) { + val summaryUpdateForRemoval = summaryUpdate?.also { + it.shouldHeadsUpEver = false + } ?: PostedEntry(logicalSummary, + wasAdded = false, + wasUpdated = false, + shouldHeadsUpEver = false, + shouldHeadsUpAgain = false, + isAlerting = mHeadsUpManager.isAlerting(logicalSummary.key), + isBinding = isEntryBinding(logicalSummary), + ) + // If we transfer the alert and the summary isn't even attached, that means we + // should ensure the summary is no longer alerting, so we remove it here. + handlePostedEntry(summaryUpdateForRemoval, "detached-summary-remove-alert") + } else if (summaryUpdate!=null) { + mLogger.logPostedEntryWillNotEvaluate(summaryUpdate, "attached-summary-transferred") + } + + // Handle all posted entries -- if the child receiving the parent's alert is in the + // list, then set its flags to ensure it alerts. + var didAlertChildToReceiveParentAlert = false + postedEntries.asSequence() + .filter { it.key != logicalSummary.key } + .forEach { postedEntry -> + if (childToReceiveParentAlert.key == postedEntry.key) { + // Update the child's posted update so that it + postedEntry.shouldHeadsUpEver = true + postedEntry.shouldHeadsUpAgain = true + handlePostedEntry(postedEntry, "child-alert-transfer-target-$targetType") + didAlertChildToReceiveParentAlert = true + } else { + handlePostedEntry(postedEntry, "child-alert-non-target") + } + } + + // If the child receiving the alert was not updated on this tick (which can happen in a + // standard alert transfer scenario), then construct an update so that we can apply it. + if (!didAlertChildToReceiveParentAlert) { + val posted = PostedEntry( + childToReceiveParentAlert, + wasAdded = false, + wasUpdated = false, + shouldHeadsUpEver = true, + shouldHeadsUpAgain = true, + isAlerting = mHeadsUpManager.isAlerting(childToReceiveParentAlert.key), + isBinding = isEntryBinding(childToReceiveParentAlert), + ) + handlePostedEntry(posted, "non-posted-child-alert-transfer-target-$targetType") + } + } + // After this method runs, all posted entries should have been handled (or skipped). + mPostedEntries.clear() + } + + /** + * Find the posted child with the newest when, and return it if it is isolated and has + * GROUP_ALERT_SUMMARY so that it can be alerted. + */ + private fun findAlertOverride( + postedEntries: List, + locationLookupByKey: (String) -> GroupLocation, + ): NotificationEntry? = postedEntries.asSequence() + .filter { posted -> !posted.entry.sbn.notification.isGroupSummary } + .sortedBy { posted -> -posted.entry.sbn.notification.`when` } + .firstOrNull() + ?.let { posted -> + posted.entry.takeIf { entry -> + locationLookupByKey(entry.key) == GroupLocation.Isolated + && entry.sbn.notification.groupAlertBehavior == GROUP_ALERT_SUMMARY + } + } + + /** + * Of children which are attached, look for the child to receive the notification: + * First prefer children which were updated, then looking for the ones with the newest 'when' + */ + private fun findBestTransferChild( + logicalMembers: List, + locationLookupByKey: (String) -> GroupLocation, + ): NotificationEntry? = logicalMembers.asSequence() + .filter { !it.sbn.notification.isGroupSummary } + .filter { locationLookupByKey(it.key) != GroupLocation.Detached } + .sortedWith(compareBy( + { !mPostedEntries.contains(it.key) }, + { -it.sbn.notification.`when` }, + )) + .firstOrNull() + + private fun getGroupLocationsByKey(list: List): Map = + mutableMapOf().also { map -> + list.forEach { topLevelEntry -> + when (topLevelEntry) { + is NotificationEntry -> map[topLevelEntry.key] = GroupLocation.Isolated + is GroupEntry -> { + topLevelEntry.summary?.let { summary -> + map[summary.key] = GroupLocation.Summary + } + topLevelEntry.children.forEach { child -> + map[child.key] = GroupLocation.Child + } + } + else -> error("unhandled type $topLevelEntry") + } + } + } + + private val mPostedEntries = LinkedHashMap() + + fun handlePostedEntry(posted: PostedEntry, scenario: String) { + mLogger.logPostedEntryWillEvaluate(posted, scenario) + if (posted.wasAdded) { + if (posted.shouldHeadsUpEver) { + bindForAsyncHeadsUp(posted) + } + } else { + if (posted.isHeadsUpAlready) { + // NOTE: This might be because we're alerting (i.e. tracked by HeadsUpManager) OR + // it could be because we're binding, and that will affect the next step. + if (posted.shouldHeadsUpEver) { + // If alerting, we need to post an update. Otherwise we're still binding, + // and we can just let that finish. + if (posted.isAlerting) { + mHeadsUpManager.updateNotification(posted.key, posted.shouldHeadsUpAgain) + } + } else { + if (posted.isAlerting) { + // We don't want this to be interrupting anymore, let's remove it + mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/) + } else { + // Don't let the bind finish + cancelHeadsUpBind(posted.entry) + } + } + } else if (posted.shouldHeadsUpEver && posted.shouldHeadsUpAgain) { + // This notification was updated to be heads up, show it! + bindForAsyncHeadsUp(posted) + } + } + } + + private fun cancelHeadsUpBind(entry: NotificationEntry) { + mEntriesBindingUntil.remove(entry.key) + mHeadsUpViewBinder.abortBindCallback(entry) + } + + private fun bindForAsyncHeadsUp(posted: PostedEntry) { + // TODO: Add a guarantee to bindHeadsUpView of some kind of callback if the bind is + // cancelled so that we don't need to have this sad timeout hack. + mEntriesBindingUntil[posted.key] = mNow + BIND_TIMEOUT + mHeadsUpViewBinder.bindHeadsUpView(posted.entry, this::onHeadsUpViewBound) } private val mNotifCollectionListener = object : NotifCollectionListener { @@ -82,9 +342,17 @@ class HeadsUpCoordinator @Inject constructor( * Notification was just added and if it should heads up, bind the view and then show it. */ override fun onEntryAdded(entry: NotificationEntry) { - if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) { - mHeadsUpViewBinder.bindHeadsUpView(entry) { entry -> onHeadsUpViewBound(entry) } - } + // shouldHeadsUp includes check for whether this notification should be filtered + val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry) + mPostedEntries[entry.key] = PostedEntry( + entry, + wasAdded = true, + wasUpdated = false, + shouldHeadsUpEver = shouldHeadsUpEver, + shouldHeadsUpAgain = true, + isAlerting = false, + isBinding = false, + ) } /** @@ -93,22 +361,26 @@ class HeadsUpCoordinator @Inject constructor( * up again. */ override fun onEntryUpdated(entry: NotificationEntry) { - val hunAgain = HeadsUpController.alertAgain(entry, entry.sbn.notification) - // includes check for whether this notification should be filtered: - val shouldHeadsUp = mNotificationInterruptStateProvider.shouldHeadsUp(entry) - val wasHeadsUp = mHeadsUpManager.isAlerting(entry.key) - if (wasHeadsUp) { - if (shouldHeadsUp) { - mHeadsUpManager.updateNotification(entry.key, hunAgain) - } else { - // We don't want this to be interrupting anymore, let's remove it - mHeadsUpManager.removeNotification( - entry.key, false /* removeImmediately */ - ) - } - } else if (shouldHeadsUp && hunAgain) { - // This notification was updated to be heads up, show it! - mHeadsUpViewBinder.bindHeadsUpView(entry) { entry -> onHeadsUpViewBound(entry) } + val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry) + val shouldHeadsUpAgain = shouldHunAgain(entry) + val isAlerting = mHeadsUpManager.isAlerting(entry.key) + val isBinding = isEntryBinding(entry) + mPostedEntries.compute(entry.key) { _, value -> + value?.also { update -> + update.wasUpdated = true + update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver + update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain + update.isAlerting = isAlerting + update.isBinding = isBinding + } ?: PostedEntry( + entry, + wasAdded = false, + wasUpdated = true, + shouldHeadsUpEver = shouldHeadsUpEver, + shouldHeadsUpAgain = shouldHeadsUpAgain, + isAlerting = isAlerting, + isBinding = isBinding, + ) } } @@ -116,8 +388,12 @@ class HeadsUpCoordinator @Inject constructor( * Stop alerting HUNs that are removed from the notification collection */ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + mPostedEntries.remove(entry.key) + cancelHeadsUpBind(entry) val entryKey = entry.key if (mHeadsUpManager.isAlerting(entryKey)) { + // TODO: This should probably know the RemoteInputCoordinator's conditions, + // or otherwise reference that coordinator's state, rather than replicate its logic val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) && !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY) mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput) @@ -129,6 +405,14 @@ class HeadsUpCoordinator @Inject constructor( } } + /** + * Checks whether an update for a notification warrants an alert for the user. + */ + private fun shouldHunAgain(entry: NotificationEntry): Boolean { + return (!entry.hasInterrupted() || + (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0) + } + private val mLifetimeExtender = object : NotifLifetimeExtender { override fun getName() = TAG @@ -164,11 +448,13 @@ class HeadsUpCoordinator @Inject constructor( private val mNotifPromoter = object : NotifPromoter(TAG) { override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean = - isCurrentlyShowingHun(entry) + isGoingToShowHunNoRetract(entry) } val sectioner = object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) { - override fun isInSection(entry: ListEntry): Boolean = isCurrentlyShowingHun(entry) + override fun isInSection(entry: ListEntry): Boolean = + // TODO: This check won't notice if a child of the group is going to HUN... + isGoingToShowHunNoRetract(entry) override fun getHeaderNodeController(): NodeController? = // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController @@ -186,7 +472,34 @@ class HeadsUpCoordinator @Inject constructor( private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key) - private fun isCurrentlyShowingHun(entry: ListEntry) = mHeadsUpManager.isAlerting(entry.key) + private fun isEntryBinding(entry: ListEntry): Boolean { + val bindingUntil = mEntriesBindingUntil[entry.key] + return bindingUntil != null && bindingUntil >= mNow + } + + /** + * Whether the notification is already alerting or binding so that it can imminently alert + */ + private fun isAttemptingToShowHun(entry: ListEntry) = + mHeadsUpManager.isAlerting(entry.key) || isEntryBinding(entry) + + /** + * Whether the notification is already alerting/binding per [isAttemptingToShowHun] OR if it + * has been updated so that it should alert this update. This method is permissive because it + * returns `true` even if the update would (in isolation of its group) cause the alert to be + * retracted. This is important for not retracting transferred group alerts. + */ + private fun isGoingToShowHunNoRetract(entry: ListEntry) = + mPostedEntries[entry.key]?.calculateShouldBeHeadsUpNoRetract ?: isAttemptingToShowHun(entry) + + /** + * If the notification has been updated, then whether it should HUN in isolation, otherwise + * defers to the already alerting/binding state of [isAttemptingToShowHun]. This method is + * strict because any update which would revoke the alert supersedes the current + * alerting/binding state. + */ + private fun isGoingToShowHunStrict(entry: ListEntry) = + mPostedEntries[entry.key]?.calculateShouldBeHeadsUpStrict ?: isAttemptingToShowHun(entry) private fun endNotifLifetimeExtensionIfExtended(entry: NotificationEntry) { if (mNotifsExtendingLifetime.remove(entry)) { @@ -196,5 +509,29 @@ class HeadsUpCoordinator @Inject constructor( companion object { private const val TAG = "HeadsUpCoordinator" + private const val BIND_TIMEOUT = 1000L } -} \ No newline at end of file + + data class PostedEntry( + val entry: NotificationEntry, + val wasAdded: Boolean, + var wasUpdated: Boolean, + var shouldHeadsUpEver: Boolean, + var shouldHeadsUpAgain: Boolean, + var isAlerting: Boolean, + var isBinding: Boolean, + ) { + val key = entry.key + val isHeadsUpAlready: Boolean + get() = isAlerting || isBinding + val calculateShouldBeHeadsUpStrict: Boolean + get() = shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain || isHeadsUpAlready) + val calculateShouldBeHeadsUpNoRetract: Boolean + get() = isHeadsUpAlready || (shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain)) + } +} + +private enum class GroupLocation { Detached, Isolated, Summary, Child } + +private fun Map.getLocation(key: String): GroupLocation = + getOrDefault(key, GroupLocation.Detached) 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 new file mode 100644 index 0000000000000..204a494c32e8a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -0,0 +1,62 @@ +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.util.Log +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.dagger.NotificationHeadsUpLog +import javax.inject.Inject + +private const val TAG = "HeadsUpCoordinator" + +class HeadsUpCoordinatorLogger constructor( + private val buffer: LogBuffer, + private val verbose: Boolean, +) { + @Inject + constructor(@NotificationHeadsUpLog buffer: LogBuffer) : + this(buffer, Log.isLoggable(TAG, Log.VERBOSE)) + + fun logPostedEntryWillEvaluate(posted: HeadsUpCoordinator.PostedEntry, reason: String) { + if (!verbose) return + buffer.log(TAG, LogLevel.VERBOSE, { + str1 = posted.key + str2 = reason + bool1 = posted.shouldHeadsUpEver + bool2 = posted.shouldHeadsUpAgain + }, { + "will evaluate posted entry $str1:" + + " reason=$str2 shouldHeadsUpEver=$bool1 shouldHeadsUpAgain=$bool2" + }) + } + + fun logPostedEntryWillNotEvaluate(posted: HeadsUpCoordinator.PostedEntry, reason: String) { + if (!verbose) return + buffer.log(TAG, LogLevel.VERBOSE, { + str1 = posted.key + str2 = reason + }, { + "will not evaluate posted entry $str1: reason=$str2" + }) + } + + fun logEvaluatingGroups(numGroups: Int) { + if (!verbose) return + buffer.log(TAG, LogLevel.VERBOSE, { + int1 = numGroups + }, { + "evaluating groups for alert transfer: $int1" + }) + } + + fun logEvaluatingGroup(groupKey: String, numPostedEntries: Int, logicalGroupSize: Int) { + if (!verbose) return + buffer.log(TAG, LogLevel.VERBOSE, { + str1 = groupKey + int1 = numPostedEntries + int2 = logicalGroupSize + }, { + "evaluating group for alert transfer: $str1" + + " numPostedEntries=$int1 logicalGroupSize=$int2" + }) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt new file mode 100644 index 0000000000000..2cb19393d2c60 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 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.dump + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.LogcatEchoTracker + +/** + * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests. + */ +fun logcatLogBuffer(name: String = "EchoToLogcatLogBuffer") = + LogBuffer(name, 50, 50, LogcatEchoTrackerAlways()) + +/** + * A [LogcatEchoTracker] that always allows echoing to the logcat. + */ +class LogcatEchoTrackerAlways : LogcatEchoTracker { + override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean = true + override fun isTagLoggable(tagName: String, level: LogLevel): Boolean = true +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index c67a2331b023d..144eefb17283d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -15,14 +15,20 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator +import android.app.Notification.GROUP_ALERT_ALL +import android.app.Notification.GROUP_ALERT_SUMMARY import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder 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.listbuilder.OnBeforeFinalizeFilterListener +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.notifcollection.NotifCollectionListener @@ -32,6 +38,7 @@ import com.android.systemui.statusbar.notification.collection.render.NodeControl import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback +import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener import com.android.systemui.util.concurrency.FakeExecutor @@ -64,10 +71,13 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private lateinit var mCollectionListener: NotifCollectionListener private lateinit var mNotifPromoter: NotifPromoter private lateinit var mNotifLifetimeExtender: NotifLifetimeExtender + private lateinit var mBeforeTransformGroupsListener: OnBeforeTransformGroupsListener + private lateinit var mBeforeFinalizeFilterListener: OnBeforeFinalizeFilterListener private lateinit var mOnHeadsUpChangedListener: OnHeadsUpChangedListener private lateinit var mNotifSectioner: NotifSectioner private val mNotifPipeline: NotifPipeline = mock() + private val mLogger = HeadsUpCoordinatorLogger(logcatLogBuffer(), verbose = true) private val mHeadsUpManager: HeadsUpManager = mock() private val mHeadsUpViewBinder: HeadsUpViewBinder = mock() private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider = mock() @@ -76,12 +86,24 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private val mHeaderController: NodeController = mock() private lateinit var mEntry: NotificationEntry - private val mExecutor = FakeExecutor(FakeSystemClock()) + private lateinit var mGroupSummary: NotificationEntry + private lateinit var mGroupPriority: NotificationEntry + private lateinit var mGroupSibling1: NotificationEntry + private lateinit var mGroupSibling2: NotificationEntry + private lateinit var mGroupChild1: NotificationEntry + private lateinit var mGroupChild2: NotificationEntry + private lateinit var mGroupChild3: NotificationEntry + private val mSystemClock = FakeSystemClock() + private val mExecutor = FakeExecutor(mSystemClock) private val mHuns: ArrayList = ArrayList() + private lateinit var mHelper: NotificationGroupTestHelper @Before fun setUp() { MockitoAnnotations.initMocks(this) + mHelper = NotificationGroupTestHelper(mContext) mCoordinator = HeadsUpCoordinator( + mLogger, + mSystemClock, mHeadsUpManager, mHeadsUpViewBinder, mNotificationInterruptStateProvider, @@ -100,6 +122,12 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { mNotifLifetimeExtender = withArgCaptor { verify(mNotifPipeline).addNotificationLifetimeExtender(capture()) } + mBeforeTransformGroupsListener = withArgCaptor { + verify(mNotifPipeline).addOnBeforeTransformGroupsListener(capture()) + } + mBeforeFinalizeFilterListener = withArgCaptor { + verify(mNotifPipeline).addOnBeforeFinalizeFilterListener(capture()) + } mOnHeadsUpChangedListener = withArgCaptor { verify(mHeadsUpManager).addListener(capture()) } @@ -116,6 +144,16 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { mNotifSectioner = mCoordinator.sectioner mNotifLifetimeExtender.setCallback(mEndLifetimeExtension) mEntry = NotificationEntryBuilder().build() + // Same summary we can use for either set of children + mGroupSummary = mHelper.createSummaryNotification(GROUP_ALERT_ALL, 0, "summary", 500) + // One set of children with GROUP_ALERT_SUMMARY + mGroupPriority = mHelper.createChildNotification(GROUP_ALERT_SUMMARY, 0, "priority", 400) + mGroupSibling1 = mHelper.createChildNotification(GROUP_ALERT_SUMMARY, 1, "sibling", 300) + mGroupSibling2 = mHelper.createChildNotification(GROUP_ALERT_SUMMARY, 2, "sibling", 200) + // Another set of children with GROUP_ALERT_ALL + mGroupChild1 = mHelper.createChildNotification(GROUP_ALERT_ALL, 1, "child", 350) + mGroupChild2 = mHelper.createChildNotification(GROUP_ALERT_ALL, 2, "child", 250) + mGroupChild3 = mHelper.createChildNotification(GROUP_ALERT_ALL, 3, "child", 150) } @Test @@ -155,6 +193,34 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(true)) } + @Test + fun testPromotesAddedHUN() { + // GIVEN the current entry should heads up + whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true) + + // WHEN the notification is added but not yet binding + mCollectionListener.onEntryAdded(mEntry) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mEntry), any()) + + // THEN only promote mEntry + assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry)) + } + + @Test + fun testPromotesBindingHUN() { + // GIVEN the current entry should heads up + whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true) + + // WHEN the notification started binding on the previous run + mCollectionListener.onEntryAdded(mEntry) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), any()) + + // THEN only promote mEntry + assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry)) + } + @Test fun testPromotesCurrentHUN() { // GIVEN the current HUN is set to mEntry @@ -198,6 +264,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true) mCollectionListener.onEntryAdded(mEntry) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + verify(mHeadsUpManager, never()).showNotification(mEntry) withArgCaptor { verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), capture()) }.onBindFinished(mEntry) @@ -211,6 +280,8 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { // WHEN a notification shouldn't HUN and its inflation is finished whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false) mCollectionListener.onEntryAdded(mEntry) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) // THEN we never bind the heads up view or tell HeadsUpManager to show the notification verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mEntry), any()) @@ -235,4 +306,296 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { whenever(mHeadsUpManager.topEntry).thenReturn(entry) mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, true) } -} \ No newline at end of file + + @Test + fun testTransferIsolatedChildAlert_withGroupAlertSummary() { + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mGroupSummary, mGroupSibling1)) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupSibling1) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mGroupSibling1)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mGroupSibling1)) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupSibling1) + + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupSibling1) + } + + @Test + fun testTransferIsolatedChildAlert_withGroupAlertAll() { + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mGroupSummary, mGroupChild1)) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupChild1) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mGroupChild1)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mGroupChild1)) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupChild1) + + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupChild1) + } + + @Test + fun testTransferTwoIsolatedChildAlert_withGroupAlertSummary() { + // WHEN a notification should HUN and its inflation is finished + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupSibling1, mGroupSibling2, mGroupPriority)) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupSibling1) + mCollectionListener.onEntryAdded(mGroupSibling2) + val entryList = listOf(mGroupSibling1, mGroupSibling2) + mBeforeTransformGroupsListener.onBeforeTransformGroups(entryList) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(entryList) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupSibling1) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling2), any()) + + // THEN we tell the HeadsUpManager to show the notification + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupSibling1) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + } + + @Test + fun testTransferTwoIsolatedChildAlert_withGroupAlertAll() { + // WHEN a notification should HUN and its inflation is finished + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupChild1, mGroupChild2, mGroupPriority)) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupChild1) + mCollectionListener.onEntryAdded(mGroupChild2) + val entryList = listOf(mGroupChild1, mGroupChild2) + mBeforeTransformGroupsListener.onBeforeTransformGroups(entryList) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(entryList) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupChild1) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupChild2), any()) + + // THEN we tell the HeadsUpManager to show the notification + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupChild1) + verify(mHeadsUpManager, never()).showNotification(mGroupChild2) + } + + @Test + fun testTransferToPriorityOnAddWithTwoSiblings() { + // WHEN a notification should HUN and its inflation is finished + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupSibling1, mGroupSibling2, mGroupPriority)) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupPriority) + mCollectionListener.onEntryAdded(mGroupSibling1) + mCollectionListener.onEntryAdded(mGroupSibling2) + + val beforeTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupPriority, mGroupSibling2)) + .build() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(beforeTransformGroup)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + + val afterTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupSibling2)) + .build() + mBeforeFinalizeFilterListener + .onBeforeFinalizeFilter(listOf(mGroupPriority, afterTransformGroup)) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupPriority) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling1), any()) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling2), any()) + + // THEN we tell the HeadsUpManager to show the notification + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupPriority) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + } + + @Test + fun testTransferToPriorityOnUpdateWithTwoSiblings() { + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupSibling1, mGroupSibling2, mGroupPriority)) + + mCollectionListener.onEntryUpdated(mGroupSummary) + mCollectionListener.onEntryUpdated(mGroupPriority) + mCollectionListener.onEntryUpdated(mGroupSibling1) + mCollectionListener.onEntryUpdated(mGroupSibling2) + + val beforeTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupPriority, mGroupSibling2)) + .build() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(beforeTransformGroup)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + + val afterTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupSibling2)) + .build() + mBeforeFinalizeFilterListener + .onBeforeFinalizeFilter(listOf(mGroupPriority, afterTransformGroup)) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupPriority) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling1), any()) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling2), any()) + + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupPriority) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + } + + @Test + fun testTransferToPriorityOnUpdateWithTwoNonUpdatedSiblings() { + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupSibling1, mGroupSibling2, mGroupPriority)) + + mCollectionListener.onEntryUpdated(mGroupSummary) + mCollectionListener.onEntryUpdated(mGroupPriority) + + val beforeTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupPriority, mGroupSibling2)) + .build() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(beforeTransformGroup)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + + val afterTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupSibling2)) + .build() + mBeforeFinalizeFilterListener + .onBeforeFinalizeFilter(listOf(mGroupPriority, afterTransformGroup)) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupPriority) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling1), any()) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling2), any()) + + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupPriority) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + } + + @Test + fun testNoTransferToPriorityOnUpdateOfTwoSiblings() { + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupSibling1, mGroupSibling2, mGroupPriority)) + + mCollectionListener.onEntryUpdated(mGroupSummary) + mCollectionListener.onEntryUpdated(mGroupSibling1) + mCollectionListener.onEntryUpdated(mGroupSibling2) + + val beforeTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupPriority, mGroupSibling2)) + .build() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(beforeTransformGroup)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + + val afterTransformGroup = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupSibling2)) + .build() + mBeforeFinalizeFilterListener + .onBeforeFinalizeFilter(listOf(mGroupPriority, afterTransformGroup)) + + finishBind(mGroupSummary) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupPriority), any()) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling1), any()) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling2), any()) + + verify(mHeadsUpManager).showNotification(mGroupSummary) + verify(mHeadsUpManager, never()).showNotification(mGroupPriority) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + } + + @Test + fun testNoTransferTwoChildAlert_withGroupAlertSummary() { + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupSibling1, mGroupSibling2)) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupSibling1) + mCollectionListener.onEntryAdded(mGroupSibling2) + val groupEntry = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupSibling1, mGroupSibling2)) + .build() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(groupEntry)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + + finishBind(mGroupSummary) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling1), any()) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSibling2), any()) + + verify(mHeadsUpManager).showNotification(mGroupSummary) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) + verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + } + + @Test + fun testNoTransferTwoChildAlert_withGroupAlertAll() { + setShouldHeadsUp(mGroupSummary) + whenever(mNotifPipeline.allNotifs) + .thenReturn(listOf(mGroupSummary, mGroupChild1, mGroupChild2)) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupChild1) + mCollectionListener.onEntryAdded(mGroupChild2) + val groupEntry = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupChild1, mGroupChild2)) + .build() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(groupEntry)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + + finishBind(mGroupSummary) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupChild1), any()) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupChild2), any()) + + verify(mHeadsUpManager).showNotification(mGroupSummary) + verify(mHeadsUpManager, never()).showNotification(mGroupChild1) + verify(mHeadsUpManager, never()).showNotification(mGroupChild2) + } + + private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) { + whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) + } + + private fun finishBind(entry: NotificationEntry) { + verify(mHeadsUpManager, never()).showNotification(entry) + withArgCaptor { + verify(mHeadsUpViewBinder).bindHeadsUpView(eq(entry), capture()) + }.onBindFinished(entry) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java index ac32b9d6f01a5..47f15a1720a94 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java @@ -59,6 +59,13 @@ public final class NotificationGroupTestHelper { return createEntry(id, tag, true, groupAlertBehavior); } + public NotificationEntry createSummaryNotification( + int groupAlertBehavior, int id, String tag, long when) { + NotificationEntry entry = createSummaryNotification(groupAlertBehavior, id, tag); + entry.getSbn().getNotification().when = when; + return entry; + } + public NotificationEntry createChildNotification() { return createChildNotification(Notification.GROUP_ALERT_ALL); } @@ -71,6 +78,13 @@ public final class NotificationGroupTestHelper { return createEntry(id, tag, false, groupAlertBehavior); } + public NotificationEntry createChildNotification( + int groupAlertBehavior, int id, String tag, long when) { + NotificationEntry entry = createChildNotification(groupAlertBehavior, id, tag); + entry.getSbn().getNotification().when = when; + return entry; + } + public NotificationEntry createEntry(int id, String tag, boolean isSummary, int groupAlertBehavior) { Notification notif = new Notification.Builder(mContext, TEST_CHANNEL_ID)