1 /*
2  * Copyright (C) 2022 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.media.controls.ui.animation
18 
19 import android.animation.Animator
20 import android.testing.TestableLooper
21 import androidx.test.ext.junit.runners.AndroidJUnit4
22 import androidx.test.filters.SmallTest
23 import com.android.systemui.SysuiTestCase
24 import junit.framework.Assert.fail
25 import org.junit.After
26 import org.junit.Before
27 import org.junit.Rule
28 import org.junit.Test
29 import org.junit.runner.RunWith
30 import org.mockito.Mock
31 import org.mockito.Mockito.mock
32 import org.mockito.Mockito.never
33 import org.mockito.Mockito.times
34 import org.mockito.Mockito.verify
35 import org.mockito.Mockito.`when` as whenever
36 import org.mockito.junit.MockitoJUnit
37 
38 @SmallTest
39 @RunWith(AndroidJUnit4::class)
40 @TestableLooper.RunWithLooper(setAsMainLooper = true)
41 class MetadataAnimationHandlerTest : SysuiTestCase() {
42 
43     private interface Callback : () -> Unit
44     private lateinit var handler: MetadataAnimationHandler
45 
46     @Mock private lateinit var enterAnimator: Animator
47     @Mock private lateinit var exitAnimator: Animator
48     @Mock private lateinit var postExitCB: Callback
49     @Mock private lateinit var postEnterCB: Callback
50 
51     @JvmField @Rule val mockito = MockitoJUnit.rule()
52 
53     @Before
setUpnull54     fun setUp() {
55         handler = MetadataAnimationHandler(exitAnimator, enterAnimator)
56     }
57 
tearDownnull58     @After fun tearDown() {}
59 
60     @Test
firstBind_startsAnimationSetnull61     fun firstBind_startsAnimationSet() {
62         val cb = { fail("Unexpected callback") }
63         handler.setNext("data-1", cb, cb)
64 
65         verify(exitAnimator).start()
66     }
67 
68     @Test
executeAnimationEnd_runsCallacksnull69     fun executeAnimationEnd_runsCallacks() {
70         // We expect this first call to only start the exit animator
71         handler.setNext("data-1", postExitCB, postEnterCB)
72         verify(exitAnimator, times(1)).start()
73         verify(enterAnimator, never()).start()
74         verify(postExitCB, never()).invoke()
75         verify(postEnterCB, never()).invoke()
76 
77         // After the exit animator completes,
78         // the exit cb should run, and enter animation should start
79         handler.onAnimationEnd(exitAnimator)
80         verify(exitAnimator, times(1)).start()
81         verify(enterAnimator, times(1)).start()
82         verify(postExitCB, times(1)).invoke()
83         verify(postEnterCB, never()).invoke()
84 
85         // After the exit animator completes,
86         // the enter cb should run without other state changes
87         handler.onAnimationEnd(enterAnimator)
88         verify(exitAnimator, times(1)).start()
89         verify(enterAnimator, times(1)).start()
90         verify(postExitCB, times(1)).invoke()
91         verify(postEnterCB, times(1)).invoke()
92     }
93 
94     @Test
rebindSameData_executesFirstCallbacknull95     fun rebindSameData_executesFirstCallback() {
96         val postExitCB2 = mock(Callback::class.java)
97 
98         handler.setNext("data-1", postExitCB, postEnterCB)
99         handler.setNext("data-1", postExitCB2, postEnterCB)
100         handler.onAnimationEnd(exitAnimator)
101 
102         verify(postExitCB, times(1)).invoke()
103         verify(postExitCB2, never()).invoke()
104         verify(postEnterCB, never()).invoke()
105     }
106 
107     @Test
rebindDifferentData_executesSecondCallbacknull108     fun rebindDifferentData_executesSecondCallback() {
109         val postExitCB2 = mock(Callback::class.java)
110 
111         handler.setNext("data-1", postExitCB, postEnterCB)
112         handler.setNext("data-2", postExitCB2, postEnterCB)
113         handler.onAnimationEnd(exitAnimator)
114 
115         verify(postExitCB, never()).invoke()
116         verify(postExitCB2, times(1)).invoke()
117         verify(postEnterCB, never()).invoke()
118     }
119 
120     @Test
rebindBeforeEnterComplete_animationRestartsnull121     fun rebindBeforeEnterComplete_animationRestarts() {
122         val postExitCB2 = mock(Callback::class.java)
123         val postEnterCB2 = mock(Callback::class.java)
124 
125         // We expect this first call to only start the exit animator
126         handler.setNext("data-1", postExitCB, postEnterCB)
127         verify(exitAnimator, times(1)).start()
128         verify(enterAnimator, never()).start()
129         verify(postExitCB, never()).invoke()
130         verify(postExitCB2, never()).invoke()
131         verify(postEnterCB, never()).invoke()
132         verify(postEnterCB2, never()).invoke()
133 
134         // After the exit animator completes,
135         // the exit cb should run, and enter animation should start
136         whenever(exitAnimator.isRunning()).thenReturn(true)
137         whenever(enterAnimator.isRunning()).thenReturn(false)
138         handler.onAnimationEnd(exitAnimator)
139         verify(exitAnimator, times(1)).start()
140         verify(enterAnimator, times(1)).start()
141         verify(postExitCB, times(1)).invoke()
142         verify(postExitCB2, never()).invoke()
143         verify(postEnterCB, never()).invoke()
144         verify(postEnterCB2, never()).invoke()
145 
146         // Setting new data before the enter animator completes should not trigger
147         // the exit animator an additional time (since it's already running)
148         whenever(exitAnimator.isRunning()).thenReturn(false)
149         whenever(enterAnimator.isRunning()).thenReturn(true)
150         handler.setNext("data-2", postExitCB2, postEnterCB2)
151         verify(exitAnimator, times(1)).start()
152 
153         // Finishing the enterAnimator should cause the exitAnimator to fire again
154         // since the data change and additional time. No enterCB should be executed.
155         handler.onAnimationEnd(enterAnimator)
156         verify(exitAnimator, times(2)).start()
157         verify(enterAnimator, times(1)).start()
158         verify(postExitCB, times(1)).invoke()
159         verify(postExitCB2, never()).invoke()
160         verify(postEnterCB, never()).invoke()
161         verify(postEnterCB2, never()).invoke()
162 
163         // Continuing the sequence, this triggers the enter animator an additional time
164         handler.onAnimationEnd(exitAnimator)
165         verify(exitAnimator, times(2)).start()
166         verify(enterAnimator, times(2)).start()
167         verify(postExitCB, times(1)).invoke()
168         verify(postExitCB2, times(1)).invoke()
169         verify(postEnterCB, never()).invoke()
170         verify(postEnterCB2, never()).invoke()
171 
172         // And finally the enter animator completes,
173         // triggering the correct postEnterCallback to fire
174         handler.onAnimationEnd(enterAnimator)
175         verify(exitAnimator, times(2)).start()
176         verify(enterAnimator, times(2)).start()
177         verify(postExitCB, times(1)).invoke()
178         verify(postExitCB2, times(1)).invoke()
179         verify(postEnterCB, never()).invoke()
180         verify(postEnterCB2, times(1)).invoke()
181     }
182 
183     @Test
exitAnimationEndMultipleCalls_singleCallbackExecutionnull184     fun exitAnimationEndMultipleCalls_singleCallbackExecution() {
185         handler.setNext("data-1", postExitCB, postEnterCB)
186         handler.onAnimationEnd(exitAnimator)
187         handler.onAnimationEnd(exitAnimator)
188         handler.onAnimationEnd(exitAnimator)
189 
190         verify(postExitCB, times(1)).invoke()
191     }
192 
193     @Test
enterAnimatorEndsWithoutCallback_noAnimatiorStartnull194     fun enterAnimatorEndsWithoutCallback_noAnimatiorStart() {
195         handler.onAnimationEnd(enterAnimator)
196 
197         verify(exitAnimator, never()).start()
198         verify(enterAnimator, never()).start()
199     }
200 }
201