1 /*
<lambda>null2  * Copyright (C) 2024 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.animation
18 
19 import android.animation.AnimatorRuleRecordingSpec
20 import android.animation.AnimatorTestRuleToolkit
21 import android.animation.MotionControl
22 import android.animation.recordMotion
23 import android.graphics.Color
24 import android.graphics.PointF
25 import android.graphics.drawable.GradientDrawable
26 import android.platform.test.annotations.MotionTest
27 import android.view.ViewGroup
28 import android.widget.FrameLayout
29 import androidx.test.ext.junit.rules.ActivityScenarioRule
30 import androidx.test.filters.SmallTest
31 import com.android.systemui.SysuiTestCase
32 import com.android.systemui.activity.EmptyTestActivity
33 import com.android.systemui.concurrency.fakeExecutor
34 import com.android.systemui.kosmos.Kosmos
35 import com.android.systemui.kosmos.testScope
36 import com.android.systemui.runOnMainThreadAndWaitForIdleSync
37 import kotlin.test.assertTrue
38 import org.junit.Rule
39 import org.junit.Test
40 import org.junit.runner.RunWith
41 import platform.test.motion.MotionTestRule
42 import platform.test.motion.RecordedMotion
43 import platform.test.motion.view.DrawableFeatureCaptures
44 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
45 import platform.test.runner.parameterized.Parameters
46 import platform.test.screenshot.GoldenPathManager
47 import platform.test.screenshot.PathConfig
48 
49 @SmallTest
50 @MotionTest
51 @RunWith(ParameterizedAndroidJunit4::class)
52 class TransitionAnimatorTest(
53     private val fadeWindowBackgroundLayer: Boolean,
54     private val isLaunching: Boolean,
55     private val useSpring: Boolean,
56 ) : SysuiTestCase() {
57     companion object {
58         private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens"
59 
60         @get:Parameters(name = "fadeBackground={0}, isLaunching={1}, useSpring={2}")
61         @JvmStatic
62         val parameterValues = buildList {
63             booleanArrayOf(true, false).forEach { fadeBackground ->
64                 booleanArrayOf(true, false).forEach { isLaunching ->
65                     booleanArrayOf(true, false).forEach { useSpring ->
66                         add(arrayOf(fadeBackground, isLaunching, useSpring))
67                     }
68                 }
69             }
70         }
71     }
72 
73     private val kosmos = Kosmos()
74     private val pathManager = GoldenPathManager(context, GOLDENS_PATH, pathConfig = PathConfig())
75     private val transitionAnimator =
76         TransitionAnimator(
77             kosmos.fakeExecutor,
78             ActivityTransitionAnimator.TIMINGS,
79             ActivityTransitionAnimator.INTERPOLATORS,
80             ActivityTransitionAnimator.SPRING_TIMINGS,
81             ActivityTransitionAnimator.SPRING_INTERPOLATORS,
82         )
83     private val fade =
84         if (fadeWindowBackgroundLayer) {
85             "withFade"
86         } else {
87             "withoutFade"
88         }
89     private val direction =
90         if (isLaunching) {
91             "whenLaunching"
92         } else {
93             "whenReturning"
94         }
95     private val mode =
96         if (useSpring) {
97             "withSpring"
98         } else {
99             "withAnimator"
100         }
101 
102     @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
103     @get:Rule(order = 2) val animatorTestRule = android.animation.AnimatorTestRule(this)
104     @get:Rule(order = 3)
105     val motionRule =
106         MotionTestRule(
107             AnimatorTestRuleToolkit(animatorTestRule, kosmos.testScope) { activityRule.scenario },
108             pathManager,
109         )
110 
111     @Test
112     fun backgroundAnimationTimeSeries() {
113         val transitionContainer = createScene()
114         val backgroundLayer = createBackgroundLayer()
115         val animation = createAnimation(transitionContainer, backgroundLayer)
116 
117         val recordedMotion = record(backgroundLayer, animation)
118 
119         motionRule
120             .assertThat(recordedMotion)
121             .timeSeriesMatchesGolden("backgroundAnimationTimeSeries_${fade}_${direction}_$mode")
122     }
123 
124     private fun createScene(): ViewGroup {
125         lateinit var transitionContainer: ViewGroup
126         activityRule.scenario.onActivity { activity ->
127             transitionContainer = FrameLayout(activity)
128             activity.setContentView(transitionContainer)
129         }
130         waitForIdleSync()
131         return transitionContainer
132     }
133 
134     private fun createBackgroundLayer() =
135         GradientDrawable().apply {
136             setColor(Color.BLACK)
137             alpha = 0
138         }
139 
140     private fun createAnimation(
141         transitionContainer: ViewGroup,
142         backgroundLayer: GradientDrawable,
143     ): TransitionAnimator.Animation {
144         val controller = TestController(transitionContainer, isLaunching)
145 
146         val containerLocation = IntArray(2)
147         transitionContainer.getLocationOnScreen(containerLocation)
148         val endState =
149             TransitionAnimator.State(
150                 left = containerLocation[0],
151                 top = containerLocation[1],
152                 right = containerLocation[0] + 320,
153                 bottom = containerLocation[1] + 690,
154                 topCornerRadius = 0f,
155                 bottomCornerRadius = 0f,
156             )
157 
158         val startVelocity =
159             if (useSpring) {
160                 PointF(2500f, 30000f)
161             } else {
162                 null
163             }
164 
165         return transitionAnimator
166             .createAnimation(
167                 controller,
168                 controller.createAnimatorState(),
169                 endState,
170                 backgroundLayer,
171                 fadeWindowBackgroundLayer,
172                 startVelocity = startVelocity,
173             )
174             .apply { runOnMainThreadAndWaitForIdleSync { start() } }
175     }
176 
177     private fun record(
178         backgroundLayer: GradientDrawable,
179         animation: TransitionAnimator.Animation,
180     ): RecordedMotion {
181         val motionControl: MotionControl
182         val sampleIntervalMs: Long
183         if (useSpring) {
184             assertTrue { animation is TransitionAnimator.MultiSpringAnimation }
185             motionControl = MotionControl {
186                 awaitCondition { (animation as TransitionAnimator.MultiSpringAnimation).isDone }
187             }
188             sampleIntervalMs = 16L
189         } else {
190             assertTrue { animation is TransitionAnimator.InterpolatedAnimation }
191             motionControl = MotionControl { awaitFrames(count = 26) }
192             sampleIntervalMs = 20L
193         }
194 
195         return motionRule.recordMotion(
196             AnimatorRuleRecordingSpec(backgroundLayer, motionControl, sampleIntervalMs) {
197                 feature(DrawableFeatureCaptures.bounds, "bounds")
198                 feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
199                 feature(DrawableFeatureCaptures.alpha, "alpha")
200             }
201         )
202     }
203 }
204 
205 /**
206  * A simple implementation of [TransitionAnimator.Controller] which throws if it is called outside
207  * of the main thread.
208  */
209 private class TestController(
210     override var transitionContainer: ViewGroup,
211     override val isLaunching: Boolean,
212 ) : TransitionAnimator.Controller {
createAnimatorStatenull213     override fun createAnimatorState(): TransitionAnimator.State {
214         val containerLocation = IntArray(2)
215         transitionContainer.getLocationOnScreen(containerLocation)
216         return TransitionAnimator.State(
217             left = containerLocation[0] + 100,
218             top = containerLocation[1] + 300,
219             right = containerLocation[0] + 200,
220             bottom = containerLocation[1] + 400,
221             topCornerRadius = 10f,
222             bottomCornerRadius = 20f,
223         )
224     }
225 }
226