diff options
26 files changed, 838 insertions, 153 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 2887de38fe23..e3f93f237742 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip +import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel @@ -172,6 +173,18 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { } @Test + fun visibleChipKeys_allInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + screenRecordState.value = ScreenRecordModel.DoingNothing + mediaProjectionState.value = MediaProjectionState.NotProjecting + setNotifs(emptyList()) + + assertThat(latest).isEmpty() + } + + @Test fun primaryChip_screenRecordShow_restHidden_screenRecordShown() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording @@ -245,6 +258,20 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } + @Test + fun visibleChipKeys_screenRecordShowAndCallShow_hasBothKeys() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + val callNotificationKey = "call" + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(callNotificationKey) + + assertThat(latest) + .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .inOrder() + } + @EnableChipsModernization @Test fun chips_screenRecordAndCallActive_inThatOrder() = @@ -864,6 +891,37 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } + @Test + fun visibleChipKeys_threePromotedNotifs_topTwoInList() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + setNotifs( + listOf( + activeNotificationModel( + key = "firstNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("firstNotif").build(), + ), + activeNotificationModel( + key = "secondNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("secondNotif").build(), + ), + activeNotificationModel( + key = "thirdNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("thirdNotif").build(), + ), + ) + ) + + assertThat(latest).containsExactly("firstNotif", "secondNotif").inOrder() + } + @DisableChipsModernization @Test fun chipsLegacy_callAndPromotedNotifs_primaryIsCallSecondaryIsNotif() = @@ -957,6 +1015,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } + @Test + fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topTwoInList() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + val callNotificationKey = "call" + addOngoingCallState(callNotificationKey) + screenRecordState.value = ScreenRecordModel.Recording + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + ) + ) + + assertThat(latest) + .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .inOrder() + } + @EnableChipsModernization @Test fun chips_screenRecordAndCallAndPromotedNotif_notifInOverflow() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 609885d0214b..30983550f0f9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -549,7 +549,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() = + fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTapImmediately() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -570,8 +570,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) - // THEN HUN is hidden - verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any()) + // THEN HUN is hidden and it's hidden immediately + verify(headsUpManager) + .removeNotification(eq(entry.key), /* releaseImmediately= */ eq(true), any()) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt index dc27859df421..a2e4a328697e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt @@ -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) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt index 206eb89db94f..706885bf5dee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt @@ -21,6 +21,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.Before @@ -30,6 +32,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME) class HeadsUpAnimatorTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Before fun setUp() { context.getOrCreateTestableResources().apply { @@ -38,34 +42,64 @@ class HeadsUpAnimatorTest : SysuiTestCase() { } @Test - fun getHeadsUpYTranslation_fromBottomTrue_usesBottomAndYAbove() { - val underTest = HeadsUpAnimator(context) + fun getHeadsUpYTranslation_fromBottomTrue_hasStatusBarChipFalse_usesBottomAndYAbove() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) + underTest.stackTopMargin = 30 + underTest.headsUpAppearHeightBottom = 300 + + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = false) + + assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300) + } + + @Test + fun getHeadsUpYTranslation_fromBottomTrue_hasStatusBarChipTrue_usesBottomAndYAbove() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) underTest.stackTopMargin = 30 underTest.headsUpAppearHeightBottom = 300 - val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true) + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = true) + // fromBottom takes priority assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300) } @Test - fun getHeadsUpYTranslation_fromBottomFalse_usesTopMarginAndYAbove() { - val underTest = HeadsUpAnimator(context) + fun getHeadsUpYTranslation_fromBottomFalse_hasStatusBarChipFalse_usesTopMarginAndYAbove() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) underTest.stackTopMargin = 30 underTest.headsUpAppearHeightBottom = 300 - val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false) + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false, hasStatusBarChip = false) assertThat(yTranslation).isEqualTo(-30 - TEST_Y_ABOVE_SCREEN) } @Test + fun getHeadsUpYTranslation_fromBottomFalse_hasStatusBarChipTrue_usesTopMarginAndStatusBarHeight() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) + underTest.stackTopMargin = 30 + underTest.headsUpAppearHeightBottom = 300 + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = 75 + underTest.updateResources(context) + + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false, hasStatusBarChip = true) + + assertThat(yTranslation).isEqualTo(75 - 30) + } + + @Test fun getHeadsUpYTranslation_resourcesUpdated() { - val underTest = HeadsUpAnimator(context) + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) underTest.stackTopMargin = 30 underTest.headsUpAppearHeightBottom = 300 - val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true) + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = false) assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300) @@ -77,7 +111,12 @@ class HeadsUpAnimatorTest : SysuiTestCase() { underTest.updateResources(context) // THEN HeadsUpAnimator knows about it - assertThat(underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true)) + assertThat( + underTest.getHeadsUpYTranslation( + isHeadsUpFromBottom = true, + hasStatusBarChip = false, + ) + ) .isEqualTo(newYAbove + 300) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 954515015fd9..08ecbac1582c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.statusbar.notification.stack import android.annotation.DimenRes import android.content.pm.PackageManager +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.widget.FrameLayout import androidx.test.filters.SmallTest @@ -19,6 +20,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.RoundableState import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -32,6 +34,8 @@ import com.android.systemui.statusbar.notification.headsup.NotificationsHunShare import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy +import com.android.systemui.testKosmos import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat import org.junit.Assume @@ -53,6 +57,8 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { @JvmField @Rule var expect: Expect = Expect.create() + private val kosmos = testKosmos() + private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() private val avalancheController = mock<AvalancheController>() @@ -131,13 +137,14 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { hostView.addView(notificationRow) if (NotificationsHunSharedAnimationValues.isEnabled) { - headsUpAnimator = HeadsUpAnimator(context) + headsUpAnimator = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) } - stackScrollAlgorithm = StackScrollAlgorithm( - context, - hostView, - if (::headsUpAnimator.isInitialized) headsUpAnimator else null, - ) + stackScrollAlgorithm = + StackScrollAlgorithm( + context, + hostView, + if (::headsUpAnimator.isInitialized) headsUpAnimator else null, + ) } private fun isTv(): Boolean { @@ -450,6 +457,46 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_noStatusBarChip_hunTranslatedToTopOfScreen() { + val topMargin = 100f + ambientState.maxHeadsUpTranslation = 2000f + ambientState.stackTopMargin = topMargin.toInt() + headsUpAnimator?.stackTopMargin = topMargin.toInt() + whenever(notificationRow.intrinsicHeight).thenReturn(100) + + val statusBarHeight = 432 + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator!!.updateResources(context) + + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + whenever(notificationRow.hasStatusBarChipDuringHeadsUpAnimation()).thenReturn(false) + + resetViewStates_hunYTranslationIs( + expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen + ) + } + + @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_withStatusBarChip_hunTranslatedToBottomOfStatusBar() { + val topMargin = 100f + ambientState.maxHeadsUpTranslation = 2000f + ambientState.stackTopMargin = topMargin.toInt() + headsUpAnimator?.stackTopMargin = topMargin.toInt() + whenever(notificationRow.intrinsicHeight).thenReturn(100) + + val statusBarHeight = 432 + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator!!.updateResources(context) + + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + whenever(notificationRow.hasStatusBarChipDuringHeadsUpAnimation()).thenReturn(true) + + resetViewStates_hunYTranslationIs(expected = statusBarHeight - topMargin) + } + + @Test fun resetViewStates_hunAnimatingAway_bottomNotClipped() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt index cb4642cc21be..f6c031f54818 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -26,12 +26,15 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR +import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -46,7 +49,6 @@ import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.description import org.mockito.Mockito.eq import org.mockito.Mockito.verify -import org.mockito.kotlin.doNothing private const val VIEW_HEIGHT = 100 private const val FULL_SHADE_APPEAR_TRANSLATION = 300 @@ -60,6 +62,8 @@ class StackStateAnimatorTest : SysuiTestCase() { @get:Rule val setFlagsRule = SetFlagsRule() @get:Rule val animatorTestRule = AnimatorTestRule(this) + private val kosmos = testKosmos() + private lateinit var stackStateAnimator: StackStateAnimator private lateinit var headsUpAnimator: HeadsUpAnimator private val stackScroller: NotificationStackScrollLayout = mock() @@ -80,13 +84,14 @@ class StackStateAnimatorTest : SysuiTestCase() { whenever(view.viewState).thenReturn(viewState) if (NotificationsHunSharedAnimationValues.isEnabled) { - headsUpAnimator = HeadsUpAnimator(context) + headsUpAnimator = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) } - stackStateAnimator = StackStateAnimator( - mContext, - stackScroller, - if (::headsUpAnimator.isInitialized) headsUpAnimator else null, - ) + stackStateAnimator = + StackStateAnimator( + mContext, + stackScroller, + if (::headsUpAnimator.isInitialized) headsUpAnimator else null, + ) } @Test @@ -134,6 +139,62 @@ class StackStateAnimatorTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun startAnimationForEvents_headsUpFromTop_andHasStatusBarChipFalse() { + val statusBarHeight = 156 + val topMargin = 50f + val expectedStartY = -topMargin - HEADS_UP_ABOVE_SCREEN + + headsUpAnimator.stackTopMargin = topMargin.toInt() + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator.updateResources(context) + + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) + event.headsUpHasStatusBarChip = false + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setFinalActualHeight(VIEW_HEIGHT) + verify(view, description("should animate from the top")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* isHeadsUpCycling= */ false, + /* onEndRunnable= */ null, + ) + } + + @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun startAnimationForEvents_headsUpFromTop_andHasStatusBarChipTrue() { + val statusBarHeight = 156 + val topMargin = 50f + val expectedStartY = statusBarHeight - topMargin + + headsUpAnimator!!.stackTopMargin = topMargin.toInt() + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator!!.updateResources(context) + + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) + event.headsUpHasStatusBarChip = true + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setFinalActualHeight(VIEW_HEIGHT) + verify(view, description("should animate below status bar")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* isHeadsUpCycling= */ false, + /* onEndRunnable= */ null, + ) + } + + @Test @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME) fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOff() { val screenHeight = 2000f diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 1a30caf0150b..eae2c25d77d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -350,6 +350,26 @@ constructor( .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModelLegacy()) } + private val activeChips = + if (StatusBarChipsModernization.isEnabled) { + chips.map { it.active } + } else { + chipsLegacy.map { + val list = mutableListOf<OngoingActivityChipModel.Active>() + if (it.primary is OngoingActivityChipModel.Active) { + list.add(it.primary) + } + if (it.secondary is OngoingActivityChipModel.Active) { + list.add(it.secondary) + } + list + } + } + + /** A flow modeling just the keys for the currently visible chips. */ + val visibleChipKeys: Flow<List<String>> = + activeChips.map { chips -> chips.filter { !it.isHidden }.map { it.key } } + /** * Sort the given chip [bundle] in order of priority, and divide the chips between active, * overflow, and inactive (see [MultipleOngoingActivityChipsModel] for a description of each). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index f6e66237d438..fdb8cd871dd9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -27,10 +27,10 @@ import com.android.systemui.statusbar.chips.notification.domain.interactor.Statu import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntry -import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter @@ -459,7 +459,12 @@ constructor( } else { if (posted.isHeadsUpEntry) { // We don't want this to be interrupting anymore, let's remove it - hunMutator.removeNotification(posted.key, false /*removeImmediately*/) + // If the notification is pinned by the user, the only way a user can un-pin + // it is by tapping the status bar notification chip. Since that's a clear + // user action, we should remove the HUN immediately instead of waiting for + // any sort of minimum timeout. + val shouldRemoveImmediately = posted.isPinnedByUser + hunMutator.removeNotification(posted.key, shouldRemoveImmediately) } else { // Don't let the bind finish cancelHeadsUpBind(posted.entry) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt index 8eca16622084..c401d8212c29 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt @@ -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,17 +166,22 @@ constructor( outcome = "add next" addToNext(entry, runnable) - // Shorten headsUpEntryShowing display time - val nextIndex = nextList.indexOf(entry) - val isOnlyNextEntry = nextIndex == 0 && nextList.size == 1 - if (isOnlyNextEntry) { - // HeadsUpEntry.updateEntry recursively calls AvalancheController#update - // and goes to the isShowing case above - headsUpEntryShowing!!.updateEntry( - /* updatePostTime= */ false, - /* updateEarliestRemovalTime= */ false, - /* reason= */ "shorten duration of previously-last HUN", - ) + 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 + if (isOnlyNextEntry) { + // HeadsUpEntry.updateEntry recursively calls AvalancheController#update + // and goes to the isShowing case above + headsUpEntryShowing!!.updateEntry( + /* updatePostTime= */ false, + /* updateEarliestRemovalTime= */ false, + /* reason= */ "shorten duration of previously-last HUN", + ) + } } } outcome += getStateStr() @@ -190,6 +195,28 @@ constructor( } /** + * 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) { - headsUpManagerLogger.logAvalancheDuration( - thisKey, 500, "LOWER priority than next: ", nextKey) - return 500 + return RemainingDuration.UpdatedDuration(500).also { + headsUpManagerLogger.logAvalancheDuration( + thisKey, + duration = it, + "LOWER priority than next: ", + nextKey, + ) + } } else if (nextEntry.compareNonTimeFields(entry) == 0) { - headsUpManagerLogger.logAvalancheDuration( - thisKey, 1000, "SAME priority as next: ", nextKey) - return 1000 + return RemainingDuration.UpdatedDuration(1000).also { + headsUpManagerLogger.logAvalancheDuration( + 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 } } @@ -377,11 +447,11 @@ constructor( } private fun showNext() { - headsUpManagerLogger.logAvalancheStage("show next", key = "") + headsUpManagerLogger.logAvalancheStage("show next", key = "") headsUpEntryShowing = null if (nextList.isEmpty()) { - headsUpManagerLogger.logAvalancheStage("no more", key = "") + headsUpManagerLogger.logAvalancheStage("no more", key = "") previousHunKey = "" return } @@ -432,10 +502,12 @@ constructor( private fun getStateStr(): String { return "\n[AC state]" + - "\nshow: ${getKey(headsUpEntryShowing)}" + - "\nprevious: $previousHunKey" + - "\n$nextStr" + - "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + baseEntryMapStr() + "\n" + "\nshow: ${getKey(headsUpEntryShowing)}" + + "\nprevious: $previousHunKey" + + "\n$nextStr" + + "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + + baseEntryMapStr() + + "\n" } private val nextStr: String @@ -447,7 +519,7 @@ constructor( // This should never happen val nextMapStr = nextMap.keys.joinToString("\n ") { getKey(it) } return "next list (${nextList.size}):\n $nextListStr" + - "\nnext map (${nextMap.size}):\n $nextMapStr" + "\nnext map (${nextMap.size}):\n $nextMapStr" } fun getKey(entry: HeadsUpEntry?): String { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimationEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimationEvent.kt new file mode 100644 index 000000000000..ab8489653f50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimationEvent.kt @@ -0,0 +1,32 @@ +/* + * 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 + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow + +/** Models the data needed for a heads-up notification animation. */ +data class HeadsUpAnimationEvent( + /** The row corresponding to the heads-up notification. */ + val row: ExpandableNotificationRow, + /** + * True if this notification should do a appearance animation, false if this notification should + * do a disappear animation. + */ + val isHeadsUpAppearance: Boolean, + /** True if the status bar is showing a chip corresponding to this notification. */ + val hasStatusBarChip: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt index 177574f57c1c..b5d732117a8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt @@ -17,13 +17,18 @@ package com.android.systemui.statusbar.notification.headsup import android.content.Context +import com.android.internal.policy.SystemBarUtils import com.android.systemui.res.R +import com.android.systemui.statusbar.ui.SystemBarUtilsProxy /** * A class shared between [StackScrollAlgorithm] and [StackStateAnimator] to ensure all heads up * animations use the same animation values. + * + * @param systemBarUtilsProxy optional utility class to provide the status bar height. Typically + * null in production code and non-null in tests. */ -class HeadsUpAnimator(context: Context) { +class HeadsUpAnimator(context: Context, private val systemBarUtilsProxy: SystemBarUtilsProxy?) { init { NotificationsHunSharedAnimationValues.unsafeAssertInNewMode() } @@ -32,6 +37,7 @@ class HeadsUpAnimator(context: Context) { var stackTopMargin: Int = 0 private var headsUpAppearStartAboveScreen = context.fetchHeadsUpAppearStartAboveScreen() + private var statusBarHeight = fetchStatusBarHeight(context) /** * Returns the Y translation for a heads-up notification animation. @@ -40,7 +46,7 @@ class HeadsUpAnimator(context: Context) { * animation. For a disappear animation, the returned Y translation should be the ending value * of the animation. */ - fun getHeadsUpYTranslation(isHeadsUpFromBottom: Boolean): Int { + fun getHeadsUpYTranslation(isHeadsUpFromBottom: Boolean, hasStatusBarChip: Boolean): Int { NotificationsHunSharedAnimationValues.unsafeAssertInNewMode() if (isHeadsUpFromBottom) { @@ -48,6 +54,12 @@ class HeadsUpAnimator(context: Context) { return headsUpAppearHeightBottom + headsUpAppearStartAboveScreen } + if (hasStatusBarChip) { + // If this notification is also represented by a chip in the status bar, we don't want + // any HUN transitions to obscure that chip. + return statusBarHeight - stackTopMargin + } + // start from or end at the top of the screen return -stackTopMargin - headsUpAppearStartAboveScreen } @@ -55,9 +67,15 @@ class HeadsUpAnimator(context: Context) { /** Should be invoked when resource values may have changed. */ fun updateResources(context: Context) { headsUpAppearStartAboveScreen = context.fetchHeadsUpAppearStartAboveScreen() + statusBarHeight = fetchStatusBarHeight(context) } private fun Context.fetchHeadsUpAppearStartAboveScreen(): Int { return this.resources.getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen) } + + private fun fetchStatusBarHeight(context: Context): Int { + return systemBarUtilsProxy?.getStatusBarHeight() + ?: SystemBarUtils.getStatusBarHeight(context) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index d16ad80ca8b9..a07223fdb000 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -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,10 +1290,17 @@ 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 */ private boolean 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)"); @@ -1696,21 +1736,6 @@ public class HeadsUpManagerImpl } /** - * @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 */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt index 388d357b3b15..00b05cbd7bec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt @@ -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 }, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/RemainingDuration.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/RemainingDuration.kt new file mode 100644 index 000000000000..fd7f4e87e8e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/RemainingDuration.kt @@ -0,0 +1,26 @@ +/* + * 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 +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 66929a579eca..987068df3ee9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -99,6 +99,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarIconView; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FeedbackIcon; @@ -179,6 +180,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mIsFaded; private boolean mIsPromotedOngoing = false; + private boolean mHasStatusBarChipDuringHeadsUpAnimation = false; @Nullable public ImageModelIndex mImageModelIndex = null; @@ -2943,6 +2945,30 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setExpandable(!mIsPromotedOngoing); } + /** + * Sets whether the status bar is showing a chip corresponding to this notification. + * + * Only set when this notification's heads-up status changes since that's the only time it's + * relevant. + */ + public void setHasStatusBarChipDuringHeadsUpAnimation(boolean hasStatusBarChip) { + if (StatusBarNotifChips.isUnexpectedlyInLegacyMode()) { + return; + } + mHasStatusBarChipDuringHeadsUpAnimation = hasStatusBarChip; + } + + /** + * Returns true if the status bar is showing a chip corresponding to this notification during a + * heads-up appear or disappear animation. + * + * Note that this value is only set when this notification's heads-up status changes since + * that's the only time it's relevant. + */ + public boolean hasStatusBarChipDuringHeadsUpAnimation() { + return StatusBarNotifChips.isEnabled() && mHasStatusBarChipDuringHeadsUpAnimation; + } + @Override public void setClipToActualHeight(boolean clipToActualHeight) { super.setClipToActualHeight(clipToActualHeight || isUserLocked()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index f70d57653ac0..531baa8dc302 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -104,6 +104,7 @@ import com.android.systemui.shade.QSHeaderBoundsProvider; import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FakeShadowView; @@ -117,6 +118,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; +import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimationEvent; import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator; import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; @@ -139,6 +141,7 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScr import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.policy.ScrollAdapter; import com.android.systemui.statusbar.policy.SplitShadeStateController; +import com.android.systemui.statusbar.ui.SystemBarUtilsProxy; import com.android.systemui.util.Assert; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.DumpUtilsKt; @@ -351,10 +354,11 @@ public class NotificationStackScrollLayout private final int[] mTempInt2 = new int[2]; private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>(); private final HashSet<ExpandableView> mClearTransientViewsWhenFinished = new HashSet<>(); - private final HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations - = new HashSet<>(); + private final Map<ExpandableNotificationRow, HeadsUpAnimationEvent> mHeadsUpChangeAnimations + = new HashMap<>(); private boolean mForceNoOverlappingRendering; - private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>(); + private final ArrayList<ExpandableNotificationRow> mTmpHeadsUpChangeAnimations = + new ArrayList<>(); private boolean mAnimationRunning; private final ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater = new ViewTreeObserver.OnPreDrawListener() { @@ -673,7 +677,7 @@ public class NotificationStackScrollLayout mExpandHelper.setScrollAdapter(mScrollAdapter); if (NotificationsHunSharedAnimationValues.isEnabled()) { - mHeadsUpAnimator = new HeadsUpAnimator(context); + mHeadsUpAnimator = new HeadsUpAnimator(context, /* systemBarUtilsProxy= */ null); } else { mHeadsUpAnimator = null; } @@ -3074,20 +3078,20 @@ public class NotificationStackScrollLayout */ private boolean removeRemovedChildFromHeadsUpChangeAnimations(View child) { boolean hasAddEvent = false; - for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { - ExpandableNotificationRow row = eventPair.first; - boolean isHeadsUp = eventPair.second; + for (HeadsUpAnimationEvent event : mHeadsUpChangeAnimations.values()) { + ExpandableNotificationRow row = event.getRow(); + boolean isHeadsUp = event.isHeadsUpAppearance(); if (child == row) { - mTmpList.add(eventPair); + mTmpHeadsUpChangeAnimations.add(event.getRow()); hasAddEvent |= isHeadsUp; } } if (hasAddEvent) { // This child was just added lets remove all events. - mHeadsUpChangeAnimations.removeAll(mTmpList); + mTmpHeadsUpChangeAnimations.forEach((row) -> mHeadsUpChangeAnimations.remove(row)); ((ExpandableNotificationRow) child).setHeadsUpAnimatingAway(false); } - mTmpList.clear(); + mTmpHeadsUpChangeAnimations.clear(); return hasAddEvent && mAddedHeadsUpChildren.contains(child); } @@ -3373,9 +3377,9 @@ public class NotificationStackScrollLayout } private void generateHeadsUpAnimationEvents() { - for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { - ExpandableNotificationRow row = eventPair.first; - boolean isHeadsUp = eventPair.second; + for (HeadsUpAnimationEvent headsUpEvent : mHeadsUpChangeAnimations.values()) { + ExpandableNotificationRow row = headsUpEvent.getRow(); + boolean isHeadsUp = headsUpEvent.isHeadsUpAppearance(); if (isHeadsUp != row.isHeadsUp()) { // For cases where we have a heads up showing and appearing again we shouldn't // do the animations at all. @@ -3433,6 +3437,10 @@ public class NotificationStackScrollLayout } AnimationEvent event = new AnimationEvent(row, type); event.headsUpFromBottom = onBottom; + + boolean hasStatusBarChip = + StatusBarNotifChips.isEnabled() && headsUpEvent.getHasStatusBarChip(); + event.headsUpHasStatusBarChip = hasStatusBarChip; // TODO(b/283084712) remove this and update the HUN filters at creation event.filter.animateHeight = false; mAnimationEvents.add(event); @@ -5068,10 +5076,11 @@ public class NotificationStackScrollLayout mAnimationFinishedRunnables.add(runnable); } - public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) { + public void generateHeadsUpAnimation( + NotificationEntry entry, boolean isHeadsUp, boolean hasStatusBarChip) { SceneContainerFlag.assertInLegacyMode(); ExpandableNotificationRow row = entry.getHeadsUpAnimationView(); - generateHeadsUpAnimation(row, isHeadsUp); + generateHeadsUpAnimation(row, isHeadsUp, hasStatusBarChip); } /** @@ -5080,8 +5089,11 @@ public class NotificationStackScrollLayout * * @param row to animate * @param isHeadsUp true for appear, false for disappear animations + * @param hasStatusBarChip true if the status bar is currently displaying a chip for the given + * notification */ - public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { + public void generateHeadsUpAnimation( + ExpandableNotificationRow row, boolean isHeadsUp, boolean hasStatusBarChip) { boolean addAnimation = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); if (NotificationThrottleHun.isEnabled()) { @@ -5096,19 +5108,26 @@ public class NotificationStackScrollLayout : " isSeenInShade=" + row.getEntry().isSeenInShade() + " row=" + row.getKey()) + " mIsExpanded=" + mIsExpanded - + " isHeadsUp=" + isHeadsUp); + + " isHeadsUp=" + isHeadsUp + + " hasStatusBarChip=" + hasStatusBarChip); } + if (addAnimation) { // If we're hiding a HUN we just started showing THIS FRAME, then remove that event, // and do not add the disappear event either. - if (!isHeadsUp && mHeadsUpChangeAnimations.remove(new Pair<>(row, true))) { + boolean showingHunThisFrame = + mHeadsUpChangeAnimations.containsKey(row) + && mHeadsUpChangeAnimations.get(row).isHeadsUpAppearance(); + if (!isHeadsUp && showingHunThisFrame) { + mHeadsUpChangeAnimations.remove(row); if (SPEW) { Log.v(TAG, "generateHeadsUpAnimation: previous hun appear animation cancelled"); } logHunAnimationSkipped(row, "previous hun appear animation cancelled"); return; } - mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp)); + mHeadsUpChangeAnimations.put( + row, new HeadsUpAnimationEvent(row, isHeadsUp, hasStatusBarChip)); mNeedsAnimation = true; if (!mIsExpanded && !mWillExpand && !isHeadsUp) { row.setHeadsUpAnimatingAway(true); @@ -5116,6 +5135,9 @@ public class NotificationStackScrollLayout setHeadsUpAnimatingAway(true); } } + if (StatusBarNotifChips.isEnabled()) { + row.setHasStatusBarChipDuringHeadsUpAnimation(hasStatusBarChip); + } requestChildrenUpdate(); } } @@ -6702,6 +6724,7 @@ public class NotificationStackScrollLayout final long length; View viewAfterChangingView; boolean headsUpFromBottom; + boolean headsUpHasStatusBarChip; AnimationEvent(ExpandableView view, int type) { this(view, type, LENGTHS[type]); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 124e6f590bfe..bb3abc1fba38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -92,6 +92,7 @@ import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; @@ -325,6 +326,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { */ private float mMaxAlphaForGlanceableHub = 1.0f; + /** + * A list of keys for the visible status bar chips. + * + * Note that this list can contain both notification keys, as well as keys for other types of + * chips like screen recording. + */ + private List<String> mVisibleStatusBarChipKeys = new ArrayList<>(); + private final NotificationListViewBinder mViewBinder; private void updateResources() { @@ -1580,8 +1589,16 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mView.getFirstChildNotGone(); } + /** Sets the list of keys that have currently visible status bar chips. */ + public void updateStatusBarChipKeys(List<String> visibleStatusBarChipKeys) { + mVisibleStatusBarChipKeys = visibleStatusBarChipKeys; + } + public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) { - mView.generateHeadsUpAnimation(entry, isHeadsUp); + boolean hasStatusBarChip = + StatusBarNotifChips.isEnabled() + && mVisibleStatusBarChipKeys.contains(entry.getKey()); + mView.generateHeadsUpAnimation(entry, isHeadsUp, hasStatusBarChip); } public void setMaxTopPadding(int padding) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 4effb76c6570..d23a4c6307fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -1059,7 +1059,9 @@ public class StackScrollAlgorithm { shouldHunAppearFromBottom(ambientState, childState); if (NotificationsHunSharedAnimationValues.isEnabled()) { int yTranslation = - mHeadsUpAnimator.getHeadsUpYTranslation(shouldHunAppearFromBottom); + mHeadsUpAnimator.getHeadsUpYTranslation( + shouldHunAppearFromBottom, + row.hasStatusBarChipDuringHeadsUpAnimation()); childState.setYTranslation(yTranslation); } else { if (shouldHunAppearFromBottom) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index efbcaed73b62..5414318b29bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -38,6 +38,7 @@ import com.android.internal.dynamicanimation.animation.DynamicAnimation; import com.android.systemui.res.R; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator; import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues; @@ -560,7 +561,9 @@ public class StackStateAnimator { mHeadsUpAppearChildren.add(changingView); mTmpState.copyFrom(changingView.getViewState()); - mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom)); + mTmpState.setYTranslation( + getHeadsUpYTranslationStart( + event.headsUpFromBottom, event.headsUpHasStatusBarChip)); // set the height and the initial position mTmpState.applyToView(changingView); mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, @@ -672,7 +675,9 @@ public class StackStateAnimator { // StackScrollAlgorithm cannot find this view because it has been removed // from the NSSL. To correctly translate the view to the top or bottom of // the screen (where it animated from), we need to update its translation. - mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom)); + mTmpState.setYTranslation( + getHeadsUpYTranslationStart( + event.headsUpFromBottom, event.headsUpHasStatusBarChip)); endRunnable = changingView::removeFromTransientContainer; } @@ -743,9 +748,9 @@ public class StackStateAnimator { return needsCustomAnimation; } - private float getHeadsUpYTranslationStart(boolean headsUpFromBottom) { + private float getHeadsUpYTranslationStart(boolean headsUpFromBottom, boolean hasStatusBarChip) { if (NotificationsHunSharedAnimationValues.isEnabled()) { - return mHeadsUpAnimator.getHeadsUpYTranslation(headsUpFromBottom); + return mHeadsUpAnimator.getHeadsUpYTranslation(headsUpFromBottom, hasStatusBarChip); } if (headsUpFromBottom) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 1c079c198cd4..facb8941f1fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -33,6 +33,7 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController import com.android.systemui.statusbar.notification.dagger.SilentHeader @@ -133,6 +134,14 @@ constructor( } } + if (StatusBarNotifChips.isEnabled) { + launch { + viewModel.visibleStatusBarChipKeys.collect { keys -> + viewController.updateStatusBarChipKeys(keys) + } + } + } + launch { bindLogger(view) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index 5ed1889de01e..c1eb70ed7d25 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -20,6 +20,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor @@ -56,6 +57,7 @@ class NotificationListViewModel constructor( val shelf: NotificationShelfViewModel, val hideListViewModel: HideListViewModel, + val ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, val footerViewModelFactory: FooterViewModel.Factory, val emptyShadeViewModelFactory: EmptyShadeViewModel.Factory, val logger: Optional<NotificationLoggerViewModel>, @@ -364,6 +366,14 @@ constructor( } } + /** + * A list of keys for the visible status bar chips. + * + * Note that this list can contain both notification keys, as well as keys for other types of + * chips like screen recording. + */ + val visibleStatusBarChipKeys = ongoingActivityChipsViewModel.visibleChipKeys + // TODO(b/325936094) use it for the text displayed in the StatusBar fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel = HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key)) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt index feb74098f071..bc533148f514 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.ui.viewbinder +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout @@ -27,20 +29,29 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import com.android.app.tracing.coroutines.launchTraced as launch class HeadsUpNotificationViewBinder @Inject -constructor(private val viewModel: NotificationListViewModel) { +constructor( + private val viewModel: NotificationListViewModel, + private val ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, +) { suspend fun bindHeadsUpNotifications(parentView: NotificationStackScrollLayout): Unit = coroutineScope { launch { var previousKeys = emptySet<HeadsUpRowKey>() - combine(viewModel.pinnedHeadsUpRowKeys, viewModel.activeHeadsUpRowKeys, ::Pair) + combine( + viewModel.pinnedHeadsUpRowKeys, + viewModel.activeHeadsUpRowKeys, + ongoingActivityChipsViewModel.visibleChipKeys, + ::Triple, + ) .sample(viewModel.headsUpAnimationsEnabled, ::Pair) .collect { (newKeys, animationsEnabled) -> val pinned = newKeys.first val all = newKeys.second + val statusBarChips: List<String> = newKeys.third + val added = all.union(pinned) - previousKeys val removed = previousKeys - pinned previousKeys = pinned @@ -48,15 +59,23 @@ constructor(private val viewModel: NotificationListViewModel) { if (animationsEnabled) { added.forEach { key -> + val row = obtainView(key) + val hasStatusBarChip = statusBarChips.contains(row.entry.key) parentView.generateHeadsUpAnimation( - obtainView(key), + row, /* isHeadsUp = */ true, + hasStatusBarChip, ) } removed.forEach { key -> val row = obtainView(key) + val hasStatusBarChip = statusBarChips.contains(row.entry.key) if (!parentView.isBeingDragged()) { - parentView.generateHeadsUpAnimation(row, /* isHeadsUp= */ false) + parentView.generateHeadsUpAnimation( + row, + /* isHeadsUp= */ false, + hasStatusBarChip, + ) } row.markHeadsUpSeen() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index acfa94a0218b..cd2ea7d25699 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -67,6 +67,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.SourceType; @@ -1128,6 +1129,30 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { assertThat(row.mustStayOnScreen()).isFalse(); } + @Test + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + public void hasStatusBarChipDuringHeadsUpAnimation_flagOff_false() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + + row.setHasStatusBarChipDuringHeadsUpAnimation(true); + + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + public void hasStatusBarChipDuringHeadsUpAnimation_flagOn_returnsValue() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); + + row.setHasStatusBarChipDuringHeadsUpAnimation(true); + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isTrue(); + + row.setHasStatusBarChipDuringHeadsUpAnimation(false); + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); + } + private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable, Drawable rightIconDrawable) { ImageView iconView = mock(ImageView.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 2a58890f8767..8fb2a245921a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -82,6 +82,7 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +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.render.GroupExpansionManager; @@ -92,6 +93,7 @@ import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShade import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.headsup.AvalancheController; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; @@ -1260,7 +1262,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); // THEN headsUpAnimatingAway is true verify(headsUpAnimatingAwayListener).accept(true); @@ -1269,6 +1272,51 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test @EnableSceneContainer + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + public void testGenerateHeadsUpDisappearEvent_notifChipsFlagOff_statusBarChipNotSet() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ true); + + verify(row, never()).setHasStatusBarChipDuringHeadsUpAnimation(anyBoolean()); + } + + @Test + @EnableSceneContainer + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + public void testGenerateHeadsUpDisappearEvent_notifChipsFlagOn_statusBarChipSetToFalse() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); + + verify(row).setHasStatusBarChipDuringHeadsUpAnimation(false); + } + + @Test + @EnableSceneContainer + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + public void testGenerateHeadsUpDisappearEvent_notifChipsFlagOn_statusBarChipSetToTrue() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ true); + + verify(row).setHasStatusBarChipDuringHeadsUpAnimation(true); + } + + @Test + @EnableSceneContainer public void testGenerateHeadsUpDisappearEvent_stackExpanded_headsUpAnimatingAwayNotSet() { // GIVEN NSSL would be ready for HUN animations, BUT it is expanded Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); @@ -1279,7 +1327,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setHeadsUpGoingAwayAnimationsAllowed(true); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); // THEN nothing happens verify(headsUpAnimatingAwayListener, never()).accept(anyBoolean()); @@ -1294,10 +1343,12 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // BUT there is a pending appear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ true, /* hasStatusBarChip= */ false); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); // THEN nothing happens verify(headsUpAnimatingAwayListener, never()).accept(anyBoolean()); @@ -1313,7 +1364,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ true, /* hasStatusBarChip= */ false); // THEN headsUpAnimatingWay is not set verify(headsUpAnimatingAwayListener, never()).accept(anyBoolean()); @@ -1335,7 +1387,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { when(row.getEntry()).thenReturn(entry); // WHEN we generate an add event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ true, /* hasStatusBarChip= */ false); // THEN nothing happens assertThat(mStackScroller.isAddOrRemoveAnimationPending()).isFalse(); @@ -1350,7 +1403,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // AND there is a HUN animating away - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); assertTrue("a HUN should be animating away", mStackScroller.mHeadsUpAnimatingAway); // WHEN the child animations are finished diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt index fbc2a21b0888..219ecbfe963b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.emptyShadeViewModelFactory @@ -35,6 +36,7 @@ val Kosmos.notificationListViewModel by Fixture { NotificationListViewModel( notificationShelfViewModel, hideListViewModel, + ongoingActivityChipsViewModel, footerViewModelFactory, emptyShadeViewModelFactory, Optional.of(notificationListLoggerViewModel), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt index 6a995c08ecae..2c5aed40b222 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt @@ -17,7 +17,13 @@ package com.android.systemui.statusbar.notification.ui.viewbinder import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel val Kosmos.headsUpNotificationViewBinder by - Kosmos.Fixture { HeadsUpNotificationViewBinder(viewModel = notificationListViewModel) } + Kosmos.Fixture { + HeadsUpNotificationViewBinder( + viewModel = notificationListViewModel, + ongoingActivityChipsViewModel = ongoingActivityChipsViewModel, + ) + } |