Merge "New Pipeline: Group Alert"

This commit is contained in:
Jeff DeCew
2022-01-28 14:58:43 +00:00
committed by Android (Google) Code Review
5 changed files with 838 additions and 27 deletions

View File

@@ -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)

View File

@@ -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"
})
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)