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.AnimatorSet
20 import android.graphics.drawable.GradientDrawable
21 import android.platform.test.annotations.MotionTest
22 import android.view.ViewGroup
23 import android.widget.FrameLayout
24 import androidx.test.ext.junit.rules.ActivityScenarioRule
25 import androidx.test.ext.junit.runners.AndroidJUnit4
26 import androidx.test.filters.SmallTest
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.activity.EmptyTestActivity
29 import com.android.systemui.concurrency.fakeExecutor
30 import com.android.systemui.kosmos.Kosmos
31 import org.junit.Rule
32 import org.junit.Test
33 import org.junit.runner.RunWith
34 import platform.test.motion.MotionTestRule
35 import platform.test.motion.RecordedMotion
36 import platform.test.motion.view.AnimationSampling.Companion.evenlySampled
37 import platform.test.motion.view.DrawableFeatureCaptures
38 import platform.test.motion.view.ViewRecordingSpec.Companion.captureWithoutScreenshot
39 import platform.test.motion.view.ViewToolkit
40 import platform.test.motion.view.record
41 import platform.test.screenshot.DeviceEmulationRule
42 import platform.test.screenshot.DeviceEmulationSpec
43 import platform.test.screenshot.DisplaySpec
44 import platform.test.screenshot.GoldenPathManager
45 import platform.test.screenshot.PathConfig
46 
47 @SmallTest
48 @MotionTest
49 @RunWith(AndroidJUnit4::class)
50 class TransitionAnimatorTest : SysuiTestCase() {
51     companion object {
52         private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens"
53 
54         private val emulationSpec =
55             DeviceEmulationSpec(
56                 DisplaySpec(
57                     "phone",
58                     width = 320,
59                     height = 690,
60                     densityDpi = 160,
61                 )
62             )
63     }
64 
65     private val kosmos = Kosmos()
66     private val pathManager = GoldenPathManager(context, GOLDENS_PATH, pathConfig = PathConfig())
67     private val transitionAnimator =
68         TransitionAnimator(
69             kosmos.fakeExecutor,
70             ActivityTransitionAnimator.TIMINGS,
71             ActivityTransitionAnimator.INTERPOLATORS
72         )
73 
74     @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
75     @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
76     @get:Rule(order = 2)
77     val motionRule = MotionTestRule(ViewToolkit { activityRule.scenario }, pathManager)
78 
79     @Test
80     fun backgroundAnimation_whenLaunching() {
81         val backgroundLayer = GradientDrawable().apply { alpha = 0 }
82         val animator = setUpTest(backgroundLayer, isLaunching = true)
83 
84         val recordedMotion = recordMotion(backgroundLayer, animator)
85 
86         motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
87     }
88 
89     @Test
90     fun backgroundAnimation_whenReturning() {
91         val backgroundLayer = GradientDrawable().apply { alpha = 0 }
92         val animator = setUpTest(backgroundLayer, isLaunching = false)
93 
94         val recordedMotion = recordMotion(backgroundLayer, animator)
95 
96         motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
97     }
98 
99     @Test
100     fun backgroundAnimationWithoutFade_whenLaunching() {
101         val backgroundLayer = GradientDrawable().apply { alpha = 0 }
102         val animator =
103             setUpTest(backgroundLayer, isLaunching = true, fadeWindowBackgroundLayer = false)
104 
105         val recordedMotion = recordMotion(backgroundLayer, animator)
106 
107         motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
108     }
109 
110     @Test
111     fun backgroundAnimationWithoutFade_whenReturning() {
112         val backgroundLayer = GradientDrawable().apply { alpha = 0 }
113         val animator =
114             setUpTest(backgroundLayer, isLaunching = false, fadeWindowBackgroundLayer = false)
115 
116         val recordedMotion = recordMotion(backgroundLayer, animator)
117 
118         motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
119     }
120 
121     private fun setUpTest(
122         backgroundLayer: GradientDrawable,
123         isLaunching: Boolean,
124         fadeWindowBackgroundLayer: Boolean = true,
125     ): AnimatorSet {
126         lateinit var transitionContainer: ViewGroup
127         activityRule.scenario.onActivity { activity ->
128             transitionContainer = FrameLayout(activity).apply { setBackgroundColor(0x00FF00) }
129             activity.setContentView(transitionContainer)
130         }
131         waitForIdleSync()
132 
133         val controller = TestController(transitionContainer, isLaunching)
134         val animator =
135             transitionAnimator.createAnimator(
136                 controller,
137                 createEndState(transitionContainer),
138                 backgroundLayer,
139                 fadeWindowBackgroundLayer
140             )
141         return AnimatorSet().apply {
142             duration = animator.duration
143             play(animator)
144         }
145     }
146 
147     private fun createEndState(container: ViewGroup): TransitionAnimator.State {
148         val containerLocation = IntArray(2)
149         container.getLocationOnScreen(containerLocation)
150         return TransitionAnimator.State(
151             left = containerLocation[0],
152             top = containerLocation[1],
153             right = containerLocation[0] + emulationSpec.display.width,
154             bottom = containerLocation[1] + emulationSpec.display.height,
155             topCornerRadius = 0f,
156             bottomCornerRadius = 0f
157         )
158     }
159 
160     private fun recordMotion(
161         backgroundLayer: GradientDrawable,
162         animator: AnimatorSet
163     ): RecordedMotion {
164         return motionRule.record(
165             animator,
166             backgroundLayer.captureWithoutScreenshot(evenlySampled(20)) {
167                 feature(DrawableFeatureCaptures.bounds, "bounds")
168                 feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
169                 feature(DrawableFeatureCaptures.alpha, "alpha")
170             }
171         )
172     }
173 }
174 
175 /**
176  * A simple implementation of [TransitionAnimator.Controller] which throws if it is called outside
177  * of the main thread.
178  */
179 private class TestController(
180     override var transitionContainer: ViewGroup,
181     override val isLaunching: Boolean
182 ) : TransitionAnimator.Controller {
createAnimatorStatenull183     override fun createAnimatorState(): TransitionAnimator.State {
184         val containerLocation = IntArray(2)
185         transitionContainer.getLocationOnScreen(containerLocation)
186         return TransitionAnimator.State(
187             left = containerLocation[0] + 100,
188             top = containerLocation[1] + 300,
189             right = containerLocation[0] + 200,
190             bottom = containerLocation[1] + 400,
191             topCornerRadius = 10f,
192             bottomCornerRadius = 20f
193         )
194     }
195 }
196