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

Commit 754da569 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Fix Avalanche HUN replacement when critical HUN arrives" into main

parents 438efc55 ccd82b37
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