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 android.animation
18
19 import android.animation.AnimatorTestRuleToolkit.Companion.TAG
20 import android.graphics.Bitmap
21 import android.graphics.drawable.Drawable
22 import android.util.Log
23 import android.view.View
24 import androidx.core.graphics.drawable.toBitmap
25 import androidx.test.core.app.ActivityScenario
26 import java.util.concurrent.TimeUnit
27 import kotlinx.coroutines.ExperimentalCoroutinesApi
28 import kotlinx.coroutines.Job
29 import kotlinx.coroutines.flow.MutableStateFlow
30 import kotlinx.coroutines.flow.asStateFlow
31 import kotlinx.coroutines.flow.take
32 import kotlinx.coroutines.flow.takeWhile
33 import kotlinx.coroutines.launch
34 import kotlinx.coroutines.test.TestScope
35 import kotlinx.coroutines.test.runCurrent
36 import platform.test.motion.MotionTestRule
37 import platform.test.motion.RecordedMotion
38 import platform.test.motion.RecordedMotion.Companion.create
39 import platform.test.motion.golden.DataPoint
40 import platform.test.motion.golden.Feature
41 import platform.test.motion.golden.FrameId
42 import platform.test.motion.golden.TimeSeries
43 import platform.test.motion.golden.TimeSeriesCaptureScope
44 import platform.test.motion.golden.TimestampFrameId
45 import platform.test.screenshot.captureToBitmapAsync
46
47 class AnimatorTestRuleToolkit(
48 internal val animatorTestRule: AnimatorTestRule,
49 internal val testScope: TestScope,
50 internal val currentActivityScenario: () -> ActivityScenario<*>,
51 ) {
52 internal companion object {
53 const val TAG = "AnimatorRuleToolkit"
54 }
55 }
56
57 /** Capture utility to extract a [Bitmap] from a [drawable]. */
captureDrawablenull58 fun captureDrawable(drawable: Drawable): Bitmap {
59 val width = drawable.bounds.right - drawable.bounds.left
60 val height = drawable.bounds.bottom - drawable.bounds.top
61
62 // If either dimension is 0 this will fail, so we set it to 1 pixel instead.
63 return drawable.toBitmap(
64 width =
65 if (width > 0) {
66 width
67 } else {
68 1
69 },
70 height =
71 if (height > 0) {
72 height
73 } else {
74 1
75 },
76 )
77 }
78
79 /** Capture utility to extract a [Bitmap] from a [view]. */
captureViewnull80 fun captureView(view: View): Bitmap {
81 return view.captureToBitmapAsync().get(10, TimeUnit.SECONDS)
82 }
83
84 /**
85 * Controls the timing of the motion recording.
86 *
87 * The time series is recorded while the [recording] function is running.
88 */
89 class MotionControl(val recording: MotionControlFn)
90
91 typealias MotionControlFn = suspend MotionControlScope.() -> Unit
92
93 interface MotionControlScope {
94 /** Waits until [check] returns true. Invoked on each frame. */
awaitConditionnull95 suspend fun awaitCondition(check: () -> Boolean)
96
97 /** Waits for [count] frames to be processed. */
98 suspend fun awaitFrames(count: Int = 1)
99 }
100
101 /** Defines the sampling of features during a test run. */
102 data class AnimatorRuleRecordingSpec<T>(
103 /** The root `observing` object, available in [timeSeriesCapture]'s [TimeSeriesCaptureScope]. */
104 val captureRoot: T,
105
106 /** The timing for the recording. */
107 val motionControl: MotionControl,
108
109 /** Time interval between frame captures, in milliseconds. */
110 val frameDurationMs: Long = 16L,
111
112 /** Whether a sequence of screenshots should also be recorded. */
113 val visualCapture: ((captureRoot: T) -> Bitmap)? = null,
114
115 /** Produces the time-series, invoked on each animation frame. */
116 val timeSeriesCapture: TimeSeriesCaptureScope<T>.() -> Unit,
117 )
118
119 /** Records the time-series of the features specified in [recordingSpec]. */
120 fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion(
121 recordingSpec: AnimatorRuleRecordingSpec<T>
122 ): RecordedMotion {
123 with(toolkit.animatorTestRule) {
124 val activityScenario = toolkit.currentActivityScenario()
125 val frameIdCollector = mutableListOf<FrameId>()
126 val propertyCollector = mutableMapOf<String, MutableList<DataPoint<*>>>()
127 val screenshotCollector =
128 if (recordingSpec.visualCapture != null) {
129 mutableListOf<Bitmap>()
130 } else {
131 null
132 }
133
134 fun recordFrame(frameId: FrameId) {
135 Log.i(TAG, "recordFrame($frameId)")
136 frameIdCollector.add(frameId)
137 activityScenario.onActivity {
138 recordingSpec.timeSeriesCapture.invoke(
139 TimeSeriesCaptureScope(recordingSpec.captureRoot, propertyCollector)
140 )
141 }
142
143 val bitmap = recordingSpec.visualCapture?.invoke(recordingSpec.captureRoot)
144 if (bitmap != null) screenshotCollector!!.add(bitmap)
145 }
146
147 val motionControl =
148 MotionControlImpl(
149 toolkit.animatorTestRule,
150 toolkit.testScope,
151 recordingSpec.frameDurationMs,
152 recordingSpec.motionControl,
153 )
154
155 Log.i(TAG, "recordMotion() begin recording")
156
157 var startFrameTime: Long? = null
158 toolkit.currentActivityScenario().onActivity { startFrameTime = currentTime }
159 while (!motionControl.recordingEnded) {
160 var time: Long? = null
161 toolkit.currentActivityScenario().onActivity { time = currentTime }
162 recordFrame(TimestampFrameId(time!! - startFrameTime!!))
163 toolkit.currentActivityScenario().onActivity { motionControl.nextFrame() }
164 }
165
166 Log.i(TAG, "recordMotion() end recording")
167
168 val timeSeries =
169 TimeSeries(
170 frameIdCollector.toList(),
171 propertyCollector.entries.map { entry -> Feature(entry.key, entry.value) },
172 )
173
174 return create(timeSeries, screenshotCollector)
175 }
176 }
177
178 @OptIn(ExperimentalCoroutinesApi::class)
179 private class MotionControlImpl(
180 val animatorTestRule: AnimatorTestRule,
181 val testScope: TestScope,
182 val frameMs: Long,
183 motionControl: MotionControl,
184 ) : MotionControlScope {
185 private val recordingJob = motionControl.recording.launch()
186
187 private val frameEmitter = MutableStateFlow<Long>(0)
188 private val onFrame = frameEmitter.asStateFlow()
189
190 var recordingEnded: Boolean = false
191
nextFramenull192 fun nextFrame() {
193 animatorTestRule.advanceTimeBy(frameMs)
194
195 frameEmitter.tryEmit(animatorTestRule.currentTime)
196 testScope.runCurrent()
197
198 if (recordingJob.isCompleted) {
199 recordingEnded = true
200 }
201 }
202
awaitConditionnull203 override suspend fun awaitCondition(check: () -> Boolean) {
204 onFrame.takeWhile { !check() }.collect {}
205 }
206
awaitFramesnull207 override suspend fun awaitFrames(count: Int) {
208 onFrame.take(count).collect {}
209 }
210
launchnull211 private fun MotionControlFn.launch(): Job {
212 val function = this
213 return testScope.launch { function() }
214 }
215 }
216