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