New Pipeline: Group Alert
Fixes: 213368402 Test: atest HeadsUpCoordinatorTest Change-Id: I7b5433a1c959b57de82355c2fb03bbfcbdb21db4
This commit is contained in:
@@ -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<String, Long>()
|
||||
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<NotificationEntry>()
|
||||
|
||||
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<ListEntry>) {
|
||||
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<ListEntry>) {
|
||||
// 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<String, GroupLocation> 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<PostedEntry>,
|
||||
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<NotificationEntry>,
|
||||
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<ListEntry>): Map<String, GroupLocation> =
|
||||
mutableMapOf<String, GroupLocation>().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<String, PostedEntry>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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<String, GroupLocation>.getLocation(key: String): GroupLocation =
|
||||
getOrDefault(key, GroupLocation.Detached)
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<NotificationEntry> = 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<BindCallback> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@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<BindCallback> {
|
||||
verify(mHeadsUpViewBinder).bindHeadsUpView(eq(entry), capture())
|
||||
}.onBindFinished(entry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user