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

Commit ccd82b37 authored by Yining Liu's avatar Yining Liu
Browse files

Fix Avalanche HUN replacement when critical HUN arrives

The existing behavior: FSI and Remote Input activated HUNs are never
replaced by another HUN.
Behavior wanted: any HUN can be replaced by critical HUNs (FSI HUNs,
incoming calls, and user-pinned HUNs)

Bug: 403301297
Test: AvalancheControllerTest
Flag: com.android.systemui.avalanche_replace_hun_when_critical
Change-Id: Icaac02f6af25eace7226895bf436359409890564
parent b6d8dcd2
Loading
Loading
Loading
Loading
+90 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.os.Handler
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
@@ -32,8 +33,10 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerTestUtil.createCallEntry
import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerTestUtil.createFullScreenIntentEntry
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.phone.keyguardBypassController
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
@@ -68,6 +71,7 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase(
    }

    private val kosmos = testKosmos()
    @get:Rule val setFlagsRule = SetFlagsRule()

    // For creating mocks
    @get:Rule var rule: MockitoRule = MockitoJUnit.rule()
@@ -138,6 +142,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase(
        return testableHeadsUpManager.createHeadsUpEntry(createFullScreenIntentEntry(id, mContext))
    }

    private fun createCallHeadsUpEntry(id: Int): HeadsUpManagerImpl.HeadsUpEntry {
        return testableHeadsUpManager.createHeadsUpEntry(createCallEntry(id, mContext))
    }

    @Test
    fun testUpdate_isShowing_runsRunnable() {
        // Entry is showing
@@ -361,6 +369,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase(
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        // Next entry has lower priority
        if (AvalancheReplaceHunWhenCritical.isEnabled) {
            assertThat(showingEntry.getNextHunPriority(nextEntry))
                .isEqualTo(NextHunPriority.LowerPriority)
        }
        assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(1)

        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
@@ -377,7 +389,11 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase(
        val nextEntry = createHeadsUpEntry(id = 1)
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        // Same priority
        // Next entry has same priority
        if (AvalancheReplaceHunWhenCritical.isEnabled) {
            assertThat(showingEntry.getNextHunPriority(nextEntry))
                .isEqualTo(NextHunPriority.SamePriority)
        }
        assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(0)

        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
@@ -395,6 +411,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase(
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        // Next entry has higher priority
        if (AvalancheReplaceHunWhenCritical.isEnabled) {
            assertThat(showingEntry.getNextHunPriority(nextEntry))
                .isEqualTo(NextHunPriority.HigherPriority)
        }
        assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(-1)

        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
@@ -437,6 +457,75 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase(
        assertThat(duration).isEqualTo(RemainingDuration.HideImmediately)
    }

    @Test
    @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME)
    fun testGetDuration_currentIsFsi_nextEntryIsCriticalCall_flagOn_hideImmediately() {
        // FSI HUN Entry is showing
        val showingEntry = createFsiHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry

        // There's another entry waiting to show next and it's incoming call
        val nextEntry = createCallHeadsUpEntry(id = 1)

        //        nextEntry.requestedPinnedStatus = PinnedStatus.PinnedByUser
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        // Then: should hide immediately
        val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)

        assertThat(duration).isEqualTo(RemainingDuration.HideImmediately)
    }

    @Test
    @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME)
    fun testGetDuration_currentIsFsi_nextEntryIsFsi_flagOn_hideImmediately() {
        // FSI HUN Entry is showing
        val showingEntry = createFsiHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry

        // There's another entry waiting to show next and it's FSI
        val nextEntry = createFsiHeadsUpEntry(id = 1)
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        // Then: should hide immediately
        val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)

        assertThat(duration).isEqualTo(RemainingDuration.HideImmediately)
    }

    @Test
    @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME)
    fun testGetDuration_currentIsCall_nextEntryIsFsi_flagOn_hideImmediately() {
        // Call HUN Entry is showing
        val showingEntry = createCallHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry

        // There's another entry waiting to show next and it's FSI
        val nextEntry = createFsiHeadsUpEntry(id = 1)
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        // Then: should hide immediately
        val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)

        assertThat(duration).isEqualTo(RemainingDuration.HideImmediately)
    }

    @Test
    @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME)
    fun testGetDuration_currentIsCall_nextEntryIsCall_flagOn_hideImmediately() {
        // Call HUN Entry is showing
        val showingEntry = createCallHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry

        // There's another entry waiting to show next and it's Call
        val nextEntry = createCallHeadsUpEntry(id = 1)
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        // Then: should hide immediately
        val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
        assertThat(duration).isEqualTo(RemainingDuration.HideImmediately)
    }

    companion object {
        @JvmStatic
        @Parameters(name = "{0}")
+21 −0
Original line number Diff line number Diff line
@@ -18,8 +18,10 @@ package com.android.systemui.statusbar.notification.headsup;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Person;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;

@@ -80,4 +82,23 @@ public class HeadsUpManagerTestUtil {
                .build();
        return HeadsUpManagerTestUtil.createEntry(id, notif);
    }

    protected static NotificationEntry createCallEntry(int id, Context context) {
        final Person person = new Person.Builder()
                .setName("name")
                .setKey("abc")
                .setUri(Uri.parse("fake_uri").toString())
                .setBot(false)
                .build();

        final PendingIntent intent = PendingIntent.getActivity(
                context, 0, new Intent(),
                PendingIntent.FLAG_IMMUTABLE);

        final Notification notif = new Notification.Builder(context, "")
                .setSmallIcon(com.android.systemui.res.R.drawable.ic_person)
                .setStyle(Notification.CallStyle.forIncomingCall(person, intent, intent))
                .build();
        return HeadsUpManagerTestUtil.createEntry(id, notif);
    }
}
+148 −24
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@
 */
package com.android.systemui.statusbar.notification.headsup

import android.app.Notification
import android.os.Handler
import androidx.annotation.VisibleForTesting
import com.android.internal.logging.UiEvent
@@ -25,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import java.io.PrintWriter
import javax.inject.Inject
@@ -154,7 +156,11 @@ constructor(
        } else if (entry in nextMap) {
            outcome = "update next"
            nextMap[entry]?.add(runnable)
            if (AvalancheReplaceHunWhenCritical.isEnabled) {
                checkReplaceCurrentHun(entry)?.let { outcome = "$outcome & $it" }
            } else {
                checkNextPinnedByUser(entry)?.let { outcome = "$outcome & $it" }
            }
        } else if (headsUpEntryShowing == null) {
            outcome = "show now"
            showNow(entry, arrayListOf(runnable))
@@ -166,9 +172,14 @@ constructor(
            outcome = "add next"
            addToNext(entry, runnable)

            val nextIsPinnedByUserResult = checkNextPinnedByUser(entry)
            if (nextIsPinnedByUserResult != null) {
                outcome = "$outcome & $nextIsPinnedByUserResult"
            val nextReplacementResult =
                if (AvalancheReplaceHunWhenCritical.isEnabled) {
                    checkReplaceCurrentHun(entry)
                } else {
                    checkNextPinnedByUser(entry)
                }
            if (nextReplacementResult != null) {
                outcome = "$outcome & $nextReplacementResult"
            } else {
                // Shorten headsUpEntryShowing display time
                val nextIndex = nextList.indexOf(entry)
@@ -179,6 +190,7 @@ constructor(
                    headsUpEntryShowing!!.updateEntry(
                        /* updatePostTime= */ false,
                        /* updateEarliestRemovalTime= */ false,
                        /* ignoreSticky= */ false,
                        /* reason= */ "shorten duration of previously-last HUN",
                    )
                }
@@ -192,6 +204,7 @@ constructor(
    fun addToNext(entry: HeadsUpEntry, runnable: Runnable) {
        nextMap[entry] = arrayListOf(runnable)
        nextList.add(entry)
        headsUpManagerLogger.logAddToNext(entry.mEntry)
    }

    /**
@@ -201,6 +214,7 @@ constructor(
     * @return a string representing the outcome, or null if nothing changed.
     */
    private fun checkNextPinnedByUser(entry: HeadsUpEntry): String? {
        AvalancheReplaceHunWhenCritical.assertInLegacyMode()
        if (
            PromotedNotificationUi.isEnabled &&
                entry.requestedPinnedStatus == PinnedStatus.PinnedByUser
@@ -209,6 +223,7 @@ constructor(
            headsUpEntryShowing?.updateEntry(
                /* updatePostTime= */ false,
                /* updateEarliestRemovalTime= */ false,
                /* ignoreSticky= */ false,
                /* reason= */ string,
            )
            return string
@@ -216,6 +231,63 @@ constructor(
        return null
    }

    /**
     * Checks if the given entry should replace the showing HUN immediately.
     *
     * @return a string representing the reason for replacement, or null if should not replace.
     */
    private fun checkReplaceCurrentHun(entry: HeadsUpEntry): String? {
        if (AvalancheReplaceHunWhenCritical.isUnexpectedlyInLegacyMode()) return null
        var result: String? = null
        var ignoreSticky = false
        if (entry.hasFullScreenIntent()) {
            result = "next has FSI"
            ignoreSticky = true
        }
        if (entry.isCriticalCall()) {
            result = "$result next is critical call"
            ignoreSticky = true
        }
        if (
            PromotedNotificationUi.isEnabled &&
                entry.requestedPinnedStatus == PinnedStatus.PinnedByUser
        ) {
            result = "$result next is PinnedByUser"
            ignoreSticky = true
        }

        if (result != null) {
            headsUpEntryShowing?.updateEntry(
                /* updatePostTime= */ false,
                /* updateEarliestRemovalTime= */ false,
                /* ignoreSticky= */ ignoreSticky,
                /* reason= */ result,
            )
        }

        return result
    }

    private fun HeadsUpEntry.hasFullScreenIntent(): Boolean {
        return this.mEntry?.sbn?.notification?.fullScreenIntent != null
    }

    /**
     * Determines if the notification is for a critical call that must display on top of an active
     * input notification. The call isOngoing check is for a special case of incoming calls where
     * the call is not yet answered (see b/164291424).
     */
    private fun HeadsUpEntry.isCriticalCall(): Boolean {
        val notificationEntry = this.mEntry ?: return false
        val notification = notificationEntry.sbn.notification
        val isIncomingCall =
            notification.isStyle(Notification.CallStyle::class.java) &&
                notification.extras.getInt(Notification.EXTRA_CALL_TYPE) ==
                    Notification.CallStyle.CALL_TYPE_INCOMING
        return isIncomingCall ||
            (notificationEntry.sbn.isOngoing && Notification.CATEGORY_CALL == notification.category)
    }

    /**
     * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete
     * all Runnables associated with that entry.
@@ -352,6 +424,57 @@ constructor(
                )
            }
        }
        if (AvalancheReplaceHunWhenCritical.isEnabled) {
            return when (val nextPriority = entry.getNextHunPriority(nextEntry)) {

                // When next is critical, replace immediately
                is NextHunPriority.ReplaceImmediately -> {
                    RemainingDuration.HideImmediately.also {
                        headsUpManagerLogger.logAvalancheDuration(
                            thisKey,
                            duration = it,
                            nextPriority.message,
                            nextKey,
                        )
                    }
                }

                // When next has higher priority, wait 500ms before replacement
                is NextHunPriority.HigherPriority -> {
                    RemainingDuration.UpdatedDuration(500).also {
                        headsUpManagerLogger.logAvalancheDuration(
                            thisKey,
                            duration = it,
                            "LOWER priority than next: ",
                            nextKey,
                        )
                    }
                }

                // When next has same priority, wait 1000ms before replacement
                is NextHunPriority.SamePriority -> {
                    RemainingDuration.UpdatedDuration(1000).also {
                        headsUpManagerLogger.logAvalancheDuration(
                            thisKey,
                            duration = it,
                            "SAME priority as next: ",
                            nextKey,
                        )
                    }
                }

                // When next has lower priority, wait until auto dismiss
                is NextHunPriority.LowerPriority -> {
                    headsUpManagerLogger.logAvalancheDuration(
                        thisKey,
                        autoDismissMs,
                        "HIGHER priority than next: ",
                        nextKey,
                    )
                    return autoDismissMs
                }
            }
        } else {
            if (nextEntry.compareNonTimeFields(entry) == -1) {
                return RemainingDuration.UpdatedDuration(500).also {
                    headsUpManagerLogger.logAvalancheDuration(
@@ -380,6 +503,7 @@ constructor(
                return autoDismissMs
            }
        }
    }

    /** Return true if entry is waiting to show. */
    fun isWaiting(key: String): Boolean {
+67 −8
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import com.android.systemui.statusbar.notification.data.repository.HeadsUpReposi
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply;
@@ -1189,7 +1190,8 @@ public class HeadsUpManagerImpl
    /**
     * Determines if the notification is for a critical call that must display on top of an active
     * input notification.
     * The call isOngoing check is for a special case of incoming calls (see b/164291424).
     * The call isOngoing check is for a special case of incoming calls where the call is not yet
     * answered(see b/164291424).
     */
    private static boolean isCriticalCallNotif(NotificationEntry entry) {
        Notification n = entry.getSbn().getNotification();
@@ -1392,15 +1394,27 @@ public class HeadsUpManagerImpl
         * @param updatePostTime whether or not to refresh the post time
         */
        public void updateEntry(boolean updatePostTime, @Nullable String reason) {
            updateEntry(updatePostTime, /* updateEarliestRemovalTime= */ true, reason);
            updateEntry(
                    updatePostTime,
                    /* updateEarliestRemovalTime= */ true,
                    /* ignoreSticky= */ false,
                    reason
            );
        }

        /**
         * Updates an entry's removal time.
         *
         * @param updatePostTime            whether or not to refresh the post time
         * @param updateEarliestRemovalTime whether this update should further delay removal
         * @param ignoreSticky              whether or not to ignore sticky and avoid canceling the
         *                                  removal callback for an entry
         * @param reason                    reason for the update
         */
        public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime,
        public void updateEntry(
                boolean updatePostTime,
                boolean updateEarliestRemovalTime,
                boolean ignoreSticky,
                @Nullable String reason) {
            Runnable runnable = () -> {
                if (mEntry == null) {
@@ -1426,7 +1440,7 @@ public class HeadsUpManagerImpl
            mAvalancheController.update(this, runnable, "updateEntry reason:"
                    + reason + " updatePostTime:" + updatePostTime);

            if (isSticky()) {
            if (!ignoreSticky && isSticky()) {
                cancelAutoRemovalCallbacks("updateEntry (sticky)");
                return;
            }
@@ -1436,6 +1450,11 @@ public class HeadsUpManagerImpl
                        mAvalancheController.getDuration(this, mAutoDismissTime);

                if (remainingDuration instanceof RemainingDuration.HideImmediately) {
                    if (AvalancheReplaceHunWhenCritical.isEnabled()) {
                        // Should not throw exception if either AvalancheReplaceHunWhenCritical or
                        // PromotedNotificationUi flag is enabled
                        return 0;
                    }
                    /* Check if */ PromotedNotificationUi.isUnexpectedlyInLegacyMode();
                    return 0;
                }
@@ -1550,6 +1569,45 @@ public class HeadsUpManagerImpl
            return 0;
        }

        /**
         * Determines the priority of the next HUN, comparing to the current HUN
         */
        @NonNull
        public NextHunPriority getNextHunPriority(HeadsUpEntry nextHeadsUpEntry) {
            NotificationEntry nextEntry = nextHeadsUpEntry.mEntry;

            if (mEntry == null && nextEntry == null) {
                return NextHunPriority.SamePriority.INSTANCE;
            } else if (nextEntry == null) {
                return NextHunPriority.LowerPriority.INSTANCE;
            } else if (mEntry == null) {
                return NextHunPriority.HigherPriority.INSTANCE;
            }

            boolean currentCritical = hasFullScreenIntent(mEntry) || isCriticalCallNotif(mEntry);
            boolean nextHasFsi = hasFullScreenIntent(nextEntry);
            boolean nextIsCriticalCall = isCriticalCallNotif(nextEntry);

            if (nextHasFsi || nextIsCriticalCall) {
                // If next is critical, replace immediately regardless of current
                StringBuilder messageBuilder = new StringBuilder("Next is critical for:");
                if (nextIsCriticalCall) messageBuilder.append(" critical call");
                if (nextHasFsi) messageBuilder.append(" has FSI");
                return new NextHunPriority.ReplaceImmediately(messageBuilder.toString());
            }

            if (currentCritical) {
                return NextHunPriority.LowerPriority.INSTANCE;
            }

            if (mRemoteInputActive && !nextHeadsUpEntry.mRemoteInputActive) {
                return NextHunPriority.LowerPriority.INSTANCE;
            } else if (!mRemoteInputActive && nextHeadsUpEntry.mRemoteInputActive) {
                return NextHunPriority.HigherPriority.INSTANCE;
            }
            return NextHunPriority.SamePriority.INSTANCE;
        }

        public int compareTo(@NonNull HeadsUpEntry headsUpEntry) {
            if (mEntry == null && headsUpEntry.mEntry == null) {
                return 0;
@@ -1637,7 +1695,7 @@ public class HeadsUpManagerImpl
         * Clear any pending removal runnables.
         */
        public void cancelAutoRemovalCallbacks(@Nullable String reason) {
            Runnable runnable = () -> {
            Runnable cancellationRunnable = () -> {
                final boolean removed = cancelAutoRemovalCallbackInternal();

                if (removed) {
@@ -1646,10 +1704,11 @@ public class HeadsUpManagerImpl
            };
            if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
                mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
                mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
                mAvalancheController.update(this, cancellationRunnable,
                        reason + " cancelAutoRemovalCallbacks");
            } else {
                // Just removed
                runnable.run();
                cancellationRunnable.run();
            }
        }

+4 −0
Original line number Diff line number Diff line
@@ -204,6 +204,10 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) {
        )
    }

    fun logAddToNext(entry: NotificationEntry?) {
        buffer.log(TAG, INFO, { str1 = entry?.logKey }, { "Add to next: $str1" })
    }

    fun logRemoveEntryRequest(key: String, reason: String, isWaiting: Boolean) {
        buffer.log(
            TAG,
Loading