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

Commit aa47a2f8 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Notif] When HUN is PinnedByUser, always immediately show it.

Each HUN has a minimum earliestRemovalTime to ensure that a user sees a
HUN for long enough before a new HUN shows. However, if a user taps a
status bar chip, then we should *immediately* show that chip's HUN so
that the UI is responsive, even if there's a currently-showing HUN that
hasn't met its earliest removal time yet.

To do this, this CL:
 - Adds a `requestedPinnedStatus` field to `HeadsUpEntry`
 - Updates `AvalancheController#getDuration` to return a data class
   instead of a raw duration. If the next HUN is PinnedByUser (which is
   the state we use everywhere to know if a user tapped a status bar
   chip), then `#getDuration` returns `HideImmediately`.
 - If HeadsUpManagerImpl gets a `HideImmediately` value, it immediately
   hides the currently-showing HUN.

See demo videos in bug.

Fixes: 397505145
Bug: 364653005
Flag: com.android.systemui.status_bar_notification_chips

Test: Have 2 status bar notification chips. Tap on one chip then
immediately tap on the other chip -> verify the second HUN immediately
shows
Test: Tap on notification chip then trigger normal HUN -> verify normal
HUN still shows after 3s
Test: Send HUNs 0,1,2,3. While HUN 0 shows, tap chip to show HUN ->
verify chip HUN shows for 5 seconds, then hides (HUNs 1, 2, 3 discarded)
Test: atest HeadsUpManagerImplTest AvalancheControllerTest

Change-Id: I0c0a15147f00ec8cfda94ad22e4f5bcfe58ded17
parent 4fba341d
Loading
Loading
Loading
Loading
+78 −26
Original line number Diff line number Diff line
@@ -17,9 +17,10 @@ package com.android.systemui.statusbar.notification.headsup

import android.app.Notification
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.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.SysuiTestCase
@@ -28,6 +29,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
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
@@ -53,12 +55,18 @@ import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@SmallTest
@RunWithLooper
@RunWith(AndroidJUnit4::class)
@RunWith(ParameterizedAndroidJunit4::class)
@EnableFlags(NotificationThrottleHun.FLAG_NAME)
class AvalancheControllerTest : SysuiTestCase() {
class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase() {
    init {
        mSetFlagsRule.setFlagsParameterization(flags)
    }

    private val kosmos = testKosmos()

    // For creating mocks
@@ -72,10 +80,10 @@ class AvalancheControllerTest : SysuiTestCase() {
    // For creating TestableHeadsUpManager
    @Mock private val mAccessibilityMgr: AccessibilityManagerWrapper? = null
    private val mUiEventLoggerFake = UiEventLoggerFake()
    @Mock private lateinit var mHeadsUpManagerLogger: HeadsUpManagerLogger
    private val headsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer())
    @Mock private lateinit var mBgHandler: Handler

    private val mLogger = Mockito.spy(HeadsUpManagerLogger(logcatLogBuffer()))
    private val mLogger = Mockito.spy(headsUpManagerLogger)
    private val mGlobalSettings = FakeGlobalSettings()
    private val mSystemClock = FakeSystemClock()
    private val mExecutor = FakeExecutor(mSystemClock)
@@ -95,7 +103,7 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Initialize AvalancheController and TestableHeadsUpManager during setUp instead of
        // declaration, where mocks are null
        mAvalancheController =
            AvalancheController(dumpManager, mUiEventLoggerFake, mHeadsUpManagerLogger, mBgHandler)
            AvalancheController(dumpManager, mUiEventLoggerFake, headsUpManagerLogger, mBgHandler)

        testableHeadsUpManager =
            HeadsUpManagerImpl(
@@ -278,7 +286,7 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Delete
        mAvalancheController.delete(firstEntry, runnableMock, "testLabel")

        // Next entry is shown
        // Showing entry becomes previous
        assertThat(mAvalancheController.previousHunKey).isEqualTo(firstEntry.mEntry!!.key)
    }

@@ -296,12 +304,12 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Delete
        mAvalancheController.delete(showingEntry, runnableMock!!, "testLabel")

        // Next entry is shown
        // Previous key not filled in
        assertThat(mAvalancheController.previousHunKey).isEqualTo("")
    }

    @Test
    fun testGetDurationMs_untrackedEntryEmptyAvalanche_useAutoDismissTime() {
    fun testGetDuration_untrackedEntryEmptyAvalanche_useAutoDismissTime() {
        val givenEntry = createHeadsUpEntry(id = 0)

        // Nothing is showing
@@ -310,12 +318,12 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Nothing is next
        mAvalancheController.clearNext()

        val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000)
        assertThat(durationMs).isEqualTo(5000)
        val durationMs = mAvalancheController.getDuration(givenEntry, autoDismissMsValue = 5000)
        assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000)
    }

    @Test
    fun testGetDurationMs_untrackedEntryNonEmptyAvalanche_useAutoDismissTime() {
    fun testGetDuration_untrackedEntryNonEmptyAvalanche_useAutoDismissTime() {
        val givenEntry = createHeadsUpEntry(id = 0)

        // Given entry not tracked
@@ -325,12 +333,12 @@ class AvalancheControllerTest : SysuiTestCase() {
        val nextEntry = createHeadsUpEntry(id = 2)
        mAvalancheController.addToNext(nextEntry, runnableMock!!)

        val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000)
        assertThat(durationMs).isEqualTo(5000)
        val durationMs = mAvalancheController.getDuration(givenEntry, autoDismissMsValue = 5000)
        assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000)
    }

    @Test
    fun testGetDurationMs_lastEntry_useAutoDismissTime() {
    fun testGetDuration_lastEntry_useAutoDismissTime() {
        // Entry is showing
        val showingEntry = createHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry
@@ -338,12 +346,12 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Nothing is next
        mAvalancheController.clearNext()

        val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000)
        assertThat(durationMs).isEqualTo(5000)
        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
        assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000)
    }

    @Test
    fun testGetDurationMs_nextEntryLowerPriority_5000() {
    fun testGetDuration_nextEntryLowerPriority_5000() {
        // Entry is showing
        val showingEntry = createFsiHeadsUpEntry(id = 1)
        mAvalancheController.headsUpEntryShowing = showingEntry
@@ -355,12 +363,12 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Next entry has lower priority
        assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(1)

        val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000)
        assertThat(durationMs).isEqualTo(5000)
        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
        assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000)
    }

    @Test
    fun testGetDurationMs_nextEntrySamePriority_1000() {
    fun testGetDuration_nextEntrySamePriority_1000() {
        // Entry is showing
        val showingEntry = createHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry
@@ -372,12 +380,12 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Same priority
        assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(0)

        val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000)
        assertThat(durationMs).isEqualTo(1000)
        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
        assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(1000)
    }

    @Test
    fun testGetDurationMs_nextEntryHigherPriority_500() {
    fun testGetDuration_nextEntryHigherPriority_500() {
        // Entry is showing
        val showingEntry = createHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry
@@ -389,7 +397,51 @@ class AvalancheControllerTest : SysuiTestCase() {
        // Next entry has higher priority
        assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(-1)

        val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000)
        assertThat(durationMs).isEqualTo(500)
        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)
        assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(500)
    }

    @Test
    @DisableFlags(StatusBarNotifChips.FLAG_NAME)
    fun testGetDuration_nextEntryIsPinnedByUser_flagOff_1000() {
        // Entry is showing
        val showingEntry = createHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry

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

        val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)

        // BUT PinnedByUser is ignored because flag is off, so the duration for a SAME priority next
        // is used
        assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(1000)
    }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun testGetDuration_nextEntryIsPinnedByUser_flagOn_hideImmediately() {
        // Entry is showing
        val showingEntry = createHeadsUpEntry(id = 0)
        mAvalancheController.headsUpEntryShowing = showingEntry

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

        val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000)

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

    companion object {
        @JvmStatic
        @Parameters(name = "{0}")
        fun getParams(): List<FlagsParameterization> {
            return FlagsParameterization.allCombinationsOf(StatusBarNotifChips.FLAG_NAME)
        }
    }
}
+107 −35
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.notification.headsup

import android.os.Handler
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
@@ -24,9 +23,9 @@ import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.util.Compile
import java.io.PrintWriter
import javax.inject.Inject

@@ -155,6 +154,7 @@ constructor(
        } else if (entry in nextMap) {
            outcome = "update next"
            nextMap[entry]?.add(runnable)
            checkNextPinnedByUser(entry)?.let { outcome = "$outcome & $it" }
        } else if (headsUpEntryShowing == null) {
            outcome = "show now"
            showNow(entry, arrayListOf(runnable))
@@ -166,6 +166,10 @@ constructor(
            outcome = "add next"
            addToNext(entry, runnable)

            val nextIsPinnedByUserResult = checkNextPinnedByUser(entry)
            if (nextIsPinnedByUserResult != null) {
                outcome = "$outcome & $nextIsPinnedByUserResult"
            } else {
                // Shorten headsUpEntryShowing display time
                val nextIndex = nextList.indexOf(entry)
                val isOnlyNextEntry = nextIndex == 0 && nextList.size == 1
@@ -179,6 +183,7 @@ constructor(
                    )
                }
            }
        }
        outcome += getStateStr()
        headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome)
    }
@@ -189,6 +194,28 @@ constructor(
        nextList.add(entry)
    }

    /**
     * Checks if the given entry is requesting [PinnedStatus.PinnedByUser] status and makes the
     * correct updates if needed.
     *
     * @return a string representing the outcome, or null if nothing changed.
     */
    private fun checkNextPinnedByUser(entry: HeadsUpEntry): String? {
        if (
            StatusBarNotifChips.isEnabled &&
                entry.requestedPinnedStatus == PinnedStatus.PinnedByUser
        ) {
            val string = "next is PinnedByUser"
            headsUpEntryShowing?.updateEntry(
                /* updatePostTime= */ false,
                /* updateEarliestRemovalTime= */ false,
                /* reason= */ string,
            )
            return string
        }
        return null
    }

    /**
     * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete
     * all Runnables associated with that entry.
@@ -243,19 +270,22 @@ constructor(
            outcome = "remove showing. ${getStateStr()}"
        } else {
            runnable.run()
            outcome = "run runnable for untracked HUN " +
            outcome =
                "run runnable for untracked HUN " +
                    "(was dropped or shown when AC was disabled). ${getStateStr()}"
        }
        headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome)
    }

    /**
     * Returns duration based on
     * Returns how much longer the given entry should show based on:
     * 1) Whether HeadsUpEntry is the last one tracked by AvalancheController
     * 2) The priority of the top HUN in the next batch Used by
     *    BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration.
     * 2) The priority of the top HUN in the next batch
     *
     * Used by [HeadsUpManagerImpl.HeadsUpEntry]'s finishTimeCalculator to shorten display duration.
     */
    fun getDurationMs(entry: HeadsUpEntry?, autoDismissMs: Int): Int {
    fun getDuration(entry: HeadsUpEntry?, autoDismissMsValue: Int): RemainingDuration {
        val autoDismissMs = RemainingDuration.UpdatedDuration(autoDismissMsValue)
        if (!isEnabled()) {
            // Use default duration, like we did before AvalancheController existed
            return autoDismissMs
@@ -273,7 +303,11 @@ constructor(
        val thisKey = getKey(entry)
        if (entryList.isEmpty()) {
            headsUpManagerLogger.logAvalancheDuration(
                thisKey, autoDismissMs, "No avalanche HUNs, use default", nextKey = "")
                thisKey,
                autoDismissMs,
                "No avalanche HUNs, use default",
                nextKey = "",
            )
            return autoDismissMs
        }
        // entryList.indexOf(entry) returns -1 even when the entry is in entryList
@@ -285,28 +319,64 @@ constructor(
        }
        if (thisEntryIndex == -1) {
            headsUpManagerLogger.logAvalancheDuration(
                thisKey, autoDismissMs, "Untracked entry, use default", nextKey = "")
                thisKey,
                autoDismissMs,
                "Untracked entry, use default",
                nextKey = "",
            )
            return autoDismissMs
        }
        val nextEntryIndex = thisEntryIndex + 1
        if (nextEntryIndex >= entryList.size) {
            headsUpManagerLogger.logAvalancheDuration(
                thisKey, autoDismissMs, "Last entry, use default", nextKey = "")
                thisKey,
                autoDismissMs,
                "Last entry, use default",
                nextKey = "",
            )
            return autoDismissMs
        }
        val nextEntry = entryList[nextEntryIndex]
        val nextKey = getKey(nextEntry)

        if (
            StatusBarNotifChips.isEnabled &&
                nextEntry.requestedPinnedStatus == PinnedStatus.PinnedByUser
        ) {
            return RemainingDuration.HideImmediately.also {
                headsUpManagerLogger.logAvalancheDuration(
                    thisKey,
                    duration = it,
                    "next is PinnedByUser",
                    nextKey,
                )
            }
        }
        if (nextEntry.compareNonTimeFields(entry) == -1) {
            return RemainingDuration.UpdatedDuration(500).also {
                headsUpManagerLogger.logAvalancheDuration(
                thisKey, 500, "LOWER priority than next: ", nextKey)
            return 500
                    thisKey,
                    duration = it,
                    "LOWER priority than next: ",
                    nextKey,
                )
            }
        } else if (nextEntry.compareNonTimeFields(entry) == 0) {
            return RemainingDuration.UpdatedDuration(1000).also {
                headsUpManagerLogger.logAvalancheDuration(
                thisKey, 1000, "SAME priority as next: ", nextKey)
            return 1000
                    thisKey,
                    duration = it,
                    "SAME priority as next: ",
                    nextKey,
                )
            }
        } else {
            headsUpManagerLogger.logAvalancheDuration(
                thisKey, autoDismissMs, "HIGHER priority than next: ", nextKey)
                thisKey,
                autoDismissMs,
                "HIGHER priority than next: ",
                nextKey,
            )
            return autoDismissMs
        }
    }
@@ -435,7 +505,9 @@ constructor(
            "\nshow: ${getKey(headsUpEntryShowing)}" +
            "\nprevious: $previousHunKey" +
            "\n$nextStr" +
                "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + baseEntryMapStr() + "\n"
            "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " +
            baseEntryMapStr() +
            "\n"
    }

    private val nextStr: String
+48 −23
Original line number Diff line number Diff line
@@ -46,7 +46,6 @@ import com.android.systemui.shade.ShadeDisplayAware;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator;
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
@@ -320,15 +319,17 @@ public class HeadsUpManagerImpl

        mLogger.logShowNotificationRequest(entry, isPinnedByUser);

        PinnedStatus requestedPinnedStatus =
                isPinnedByUser
                        ? PinnedStatus.PinnedByUser
                        : PinnedStatus.PinnedBySystem;
        headsUpEntry.setRequestedPinnedStatus(requestedPinnedStatus);

        Runnable runnable = () -> {
            mLogger.logShowNotification(entry, isPinnedByUser);

            // Add new entry and begin managing it
            mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry);
            PinnedStatus requestedPinnedStatus =
                    isPinnedByUser
                            ? PinnedStatus.PinnedByUser
                            : PinnedStatus.PinnedBySystem;
            onEntryAdded(headsUpEntry, requestedPinnedStatus);
            // TODO(b/328390331) move accessibility events to the view layer
            entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
@@ -1289,9 +1290,16 @@ public class HeadsUpManagerImpl
        @Nullable private Runnable mCancelRemoveRunnable;

        private boolean mGutsShownPinned;
        /** The *current* pinned status of this HUN. */
        private final MutableStateFlow<PinnedStatus> mPinnedStatus =
                StateFlowKt.MutableStateFlow(PinnedStatus.NotPinned);

        /**
         * The *requested* pinned status of this HUN. {@link AvalancheController} uses this value to
         * know if the current HUN needs to be removed so that a pinned-by-user HUN can show.
         */
        private PinnedStatus mRequestedPinnedStatus = PinnedStatus.NotPinned;

        /**
         * If the time this entry has been on was extended
         */
@@ -1352,6 +1360,20 @@ public class HeadsUpManagerImpl
            }
        }

        /** Sets what pinned status this HUN is requesting. */
        void setRequestedPinnedStatus(PinnedStatus pinnedStatus) {
            if (!StatusBarNotifChips.isEnabled() && pinnedStatus == PinnedStatus.PinnedByUser) {
                Log.w(TAG, "PinnedByUser status not allowed if StatusBarNotifChips is disabled");
                mRequestedPinnedStatus = PinnedStatus.NotPinned;
            } else {
                mRequestedPinnedStatus = pinnedStatus;
            }
        }

        PinnedStatus getRequestedPinnedStatus() {
            return mRequestedPinnedStatus;
        }

        @VisibleForTesting
        void setRowPinnedStatus(PinnedStatus pinnedStatus) {
            if (mEntry != null) mEntry.setRowPinnedStatus(pinnedStatus);
@@ -1410,11 +1432,29 @@ public class HeadsUpManagerImpl
            }

            FinishTimeUpdater finishTimeCalculator = () -> {
                final long finishTime = calculateFinishTime();
                RemainingDuration remainingDuration =
                        mAvalancheController.getDuration(this, mAutoDismissTime);

                if (remainingDuration instanceof RemainingDuration.HideImmediately) {
                    StatusBarNotifChips.assertInNewMode();
                    return 0;
                }

                int remainingTimeoutMs;
                if (isStickyForSomeTime()) {
                    remainingTimeoutMs = mStickyForSomeTimeAutoDismissTime;
                } else {
                    remainingTimeoutMs =
                            ((RemainingDuration.UpdatedDuration) remainingDuration).getDuration();
                }
                final long duration = getRecommendedHeadsUpTimeoutMs(remainingTimeoutMs);
                final long timeoutTimestamp =
                        mPostTime + duration + (extended ? mExtensionTime : 0);

                final long now = mSystemClock.elapsedRealtime();
                return NotificationThrottleHun.isEnabled()
                        ? Math.max(finishTime, mEarliestRemovalTime) - now
                        : Math.max(finishTime - now, mMinimumDisplayTimeDefault);
                        ? Math.max(timeoutTimestamp, mEarliestRemovalTime) - now
                        : Math.max(timeoutTimestamp - now, mMinimumDisplayTimeDefault);
            };
            scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");

@@ -1695,21 +1735,6 @@ public class HeadsUpManagerImpl
            return mSystemClock.elapsedRealtime() + mTouchAcceptanceDelay;
        }

        /**
         * @return When the notification should auto-dismiss itself, based on
         * {@link SystemClock#elapsedRealtime()}
         */
        private long calculateFinishTime() {
            int requestedTimeOutMs;
            if (isStickyForSomeTime()) {
                requestedTimeOutMs = mStickyForSomeTimeAutoDismissTime;
            } else {
                requestedTimeOutMs = mAvalancheController.getDurationMs(this, mAutoDismissTime);
            }
            final long duration = getRecommendedHeadsUpTimeoutMs(requestedTimeOutMs);
            return mPostTime + duration + (extended ? mExtensionTime : 0);
        }

        /**
         * Get user-preferred or default timeout duration. The larger one will be returned.
         * @return milliseconds before auto-dismiss
+12 −2
Original line number Diff line number Diff line
@@ -106,13 +106,23 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) {
        )
    }

    fun logAvalancheDuration(thisKey: String, duration: Int, reason: String, nextKey: String) {
    fun logAvalancheDuration(
        thisKey: String,
        duration: RemainingDuration,
        reason: String,
        nextKey: String,
    ) {
        val durationMs =
            when (duration) {
                is RemainingDuration.UpdatedDuration -> duration.duration
                is RemainingDuration.HideImmediately -> 0
            }
        buffer.log(
            TAG,
            INFO,
            {
                str1 = thisKey
                int1 = duration
                int1 = durationMs
                str2 = reason
                str3 = nextKey
            },
+26 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.statusbar.notification.headsup

/** Models how much longer a HUN should be displayed. */
sealed interface RemainingDuration {
    /** This HUN should be hidden immediately, regardless of any minimum time enforcements. */
    data object HideImmediately : RemainingDuration

    /** This HUN should hide after [duration] milliseconds have occurred. */
    data class UpdatedDuration(val duration: Int) : RemainingDuration
}