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

Commit ed2a0a58 authored by Jeff DeCew's avatar Jeff DeCew Committed by Android (Google) Code Review
Browse files

Merge "New Pipeline: Group Alert"

parents 7aca652f 91181a00
Loading
Loading
Loading
Loading
+362 −25
Original line number Diff line number Diff line
@@ -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,31 +361,39 @@ 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 */
            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,
                )
            }
            } else if (shouldHeadsUp && hunAgain) {
                // This notification was updated to be heads up, show it!
                mHeadsUpViewBinder.bindHeadsUpView(entry) { entry -> onHeadsUpViewBound(entry) }
            }
        }

        /**
         * 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)
+62 −0

File added.

Preview size limit exceeded, changes collapsed.

+35 −0
Original line number Diff line number Diff line
/*
 * 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
}
+365 −2

File changed.

Preview size limit exceeded, changes collapsed.

+14 −0
Original line number Diff line number Diff line
@@ -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)