1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.notification.stack 18 19 import android.platform.test.annotations.EnableFlags 20 import android.testing.TestableLooper.RunWithLooper 21 import androidx.test.ext.junit.runners.AndroidJUnit4 22 import androidx.test.filters.SmallTest 23 import com.android.systemui.SysuiTestCase 24 import com.android.systemui.animation.AnimatorTestRule 25 import com.android.systemui.res.R 26 import com.android.systemui.statusbar.notification.row.ExpandableView 27 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation 28 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent 29 import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR 30 import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR 31 import com.android.systemui.util.mockito.argumentCaptor 32 import com.android.systemui.util.mockito.mock 33 import com.android.systemui.util.mockito.whenever 34 import com.google.common.truth.Truth.assertThat 35 import org.junit.Before 36 import org.junit.Rule 37 import org.junit.Test 38 import org.junit.runner.RunWith 39 import org.mockito.ArgumentCaptor 40 import org.mockito.Mockito.any 41 import org.mockito.Mockito.clearInvocations 42 import org.mockito.Mockito.description 43 import org.mockito.Mockito.eq 44 import org.mockito.Mockito.verify 45 46 private const val VIEW_HEIGHT = 100 47 private const val FULL_SHADE_APPEAR_TRANSLATION = 300 48 private const val HEADS_UP_ABOVE_SCREEN = 80 49 50 @SmallTest 51 @RunWith(AndroidJUnit4::class) 52 @RunWithLooper 53 class StackStateAnimatorTest : SysuiTestCase() { 54 55 @get:Rule val animatorTestRule = AnimatorTestRule(this) 56 57 private lateinit var stackStateAnimator: StackStateAnimator 58 private val stackScroller: NotificationStackScrollLayout = mock() 59 private val view: ExpandableView = mock() 60 private val viewState: ExpandableViewState = <lambda>null61 ExpandableViewState().apply { height = VIEW_HEIGHT } 62 private val runnableCaptor: ArgumentCaptor<Runnable> = argumentCaptor() 63 @Before setUpnull64 fun setUp() { 65 overrideResource( 66 R.dimen.go_to_full_shade_appearing_translation, 67 FULL_SHADE_APPEAR_TRANSLATION 68 ) 69 overrideResource(R.dimen.heads_up_appear_y_above_screen, HEADS_UP_ABOVE_SCREEN) 70 71 whenever(stackScroller.context).thenReturn(context) 72 whenever(view.viewState).thenReturn(viewState) 73 stackStateAnimator = StackStateAnimator(mContext, stackScroller) 74 } 75 76 @Test 77 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnimnull78 fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim() { 79 val topMargin = 50f 80 val expectedStartY = -topMargin - stackStateAnimator.mHeadsUpAppearStartAboveScreen 81 val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) 82 stackStateAnimator.setStackTopMargin(topMargin.toInt()) 83 84 stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) 85 86 verify(view).setActualHeight(VIEW_HEIGHT, false) 87 verify(view, description("should animate from the top")).translationY = expectedStartY 88 verify(view) 89 .performAddAnimation( 90 /* delay= */ 0L, 91 /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), 92 /* isHeadsUpAppear= */ true, 93 /* onEndRunnable= */ null 94 ) 95 } 96 97 @Test 98 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnimnull99 fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim() { 100 val screenHeight = 2000f 101 val expectedStartY = screenHeight + stackStateAnimator.mHeadsUpAppearStartAboveScreen 102 val event = 103 AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR).apply { 104 headsUpFromBottom = true 105 } 106 stackStateAnimator.setHeadsUpAppearHeightBottom(screenHeight.toInt()) 107 108 stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) 109 110 verify(view).setActualHeight(VIEW_HEIGHT, false) 111 verify(view, description("should animate from the bottom")).translationY = expectedStartY 112 verify(view) 113 .performAddAnimation( 114 /* delay= */ 0L, 115 /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), 116 /* isHeadsUpAppear= */ true, 117 /* onEndRunnable= */ null 118 ) 119 } 120 121 @Test 122 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) startAnimationForEvents_startsHeadsUpDisappearAnimnull123 fun startAnimationForEvents_startsHeadsUpDisappearAnim() { 124 val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong() 125 val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) 126 clearInvocations(view) 127 stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) 128 129 verify(view) 130 .performRemoveAnimation( 131 /* duration= */ eq(disappearDuration), 132 /* delay= */ eq(0L), 133 /* translationDirection= */ eq(0f), 134 /* isHeadsUpAnimation= */ eq(true), 135 /* onStartedRunnable= */ any(), 136 /* onFinishedRunnable= */ runnableCaptor.capture(), 137 /* animationListener= */ any(), 138 /* clipSide= */ eq(ExpandableView.ClipSide.BOTTOM), 139 ) 140 141 animatorTestRule.advanceTimeBy(disappearDuration) // move to the end of SSA animations 142 runnableCaptor.value.run() // execute the end runnable 143 144 verify(view, description("should be translated to the heads up appear start")) 145 .translationY = -stackStateAnimator.mHeadsUpAppearStartAboveScreen 146 verify(view, description("should be called at the end of the disappear animation")) 147 .removeFromTransientContainer() 148 } 149 150 @Test initView_updatesResourcesnull151 fun initView_updatesResources() { 152 // Given: the resource values are initialized in the SSA 153 assertThat(stackStateAnimator.mGoToFullShadeAppearingTranslation) 154 .isEqualTo(FULL_SHADE_APPEAR_TRANSLATION) 155 assertThat(stackStateAnimator.mHeadsUpAppearStartAboveScreen) 156 .isEqualTo(HEADS_UP_ABOVE_SCREEN) 157 158 // When: initView is called after the resources have changed 159 overrideResource(R.dimen.go_to_full_shade_appearing_translation, 200) 160 overrideResource(R.dimen.heads_up_appear_y_above_screen, 100) 161 stackStateAnimator.initView(mContext) 162 163 // Then: the resource values are updated 164 assertThat(stackStateAnimator.mGoToFullShadeAppearingTranslation).isEqualTo(200) 165 assertThat(stackStateAnimator.mHeadsUpAppearStartAboveScreen).isEqualTo(100) 166 } 167 } 168