summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/FlickerTests/Android.bp1
-rw-r--r--tests/FlickerTests/test-apps/app-helpers/Android.bp1
-rw-r--r--tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt4
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml14
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java6
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java28
-rw-r--r--tests/graphics/SilkFX/res/layout/view_blur_behind.xml148
-rw-r--r--tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt3
-rw-r--r--tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt30
-rw-r--r--tests/testables/Android.bp4
-rw-r--r--tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt159
-rw-r--r--tests/testables/tests/Android.bp4
-rw-r--r--tests/testables/tests/goldens/recordMotion_withAnimator.json64
-rw-r--r--tests/testables/tests/goldens/recordMotion_withSpring.json48
-rw-r--r--tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt129
15 files changed, 638 insertions, 5 deletions
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 27e9ffa4cea5..1e997b386faa 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -47,7 +47,6 @@ java_defaults {
java_library {
name: "wm-flicker-common-assertions",
- platform_apis: true,
optimize: {
enabled: false,
},
diff --git a/tests/FlickerTests/test-apps/app-helpers/Android.bp b/tests/FlickerTests/test-apps/app-helpers/Android.bp
index fc4d71c652d5..e8bb64aa6c55 100644
--- a/tests/FlickerTests/test-apps/app-helpers/Android.bp
+++ b/tests/FlickerTests/test-apps/app-helpers/Android.bp
@@ -25,7 +25,6 @@ package {
java_library {
name: "wm-flicker-common-app-helpers",
- platform_apis: true,
optimize: {
enabled: false,
},
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
index 634b6eedd7e6..8d27c1d1dfd1 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
@@ -33,9 +33,9 @@ class LetterboxAppHelper
@JvmOverloads
constructor(
instr: Instrumentation,
- launcherName: String = ActivityOptions.NonResizeablePortraitActivity.LABEL,
+ launcherName: String = ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.LABEL,
component: ComponentNameMatcher =
- ActivityOptions.NonResizeablePortraitActivity.COMPONENT.toFlickerComponent()
+ ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.COMPONENT.toFlickerComponent()
) : StandardAppHelper(instr, launcherName, component) {
private val gestureHelper: GestureHelper = GestureHelper(instrumentation)
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index f891606f0066..f2e34257ef01 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -115,6 +115,19 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+ <activity android:name=".NonResizeableFixedAspectRatioPortraitActivity"
+ android:theme="@style/CutoutNever"
+ android:resizeableActivity="false"
+ android:screenOrientation="portrait"
+ android:minAspectRatio="1.77"
+ android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableFixedAspectRatioPortraitActivity"
+ android:label="NonResizeableFixedAspectRatioPortraitActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
<activity android:name=".StartMediaProjectionActivity"
android:theme="@style/CutoutNever"
android:resizeableActivity="false"
@@ -143,6 +156,7 @@
<activity android:name=".LaunchTransparentActivity"
android:resizeableActivity="false"
android:screenOrientation="portrait"
+ android:minAspectRatio="1.77"
android:theme="@style/OptOutEdgeToEdge"
android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchTransparentActivity"
android:label="LaunchTransparentActivity"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index e4de2c574553..73625da9dfa5 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -85,6 +85,12 @@ public class ActivityOptions {
FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity");
}
+ public static class NonResizeableFixedAspectRatioPortraitActivity {
+ public static final String LABEL = "NonResizeableFixedAspectRatioPortraitActivity";
+ public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+ FLICKER_APP_PACKAGE + ".NonResizeableFixedAspectRatioPortraitActivity");
+ }
+
public static class StartMediaProjectionActivity {
public static final String LABEL = "StartMediaProjectionActivity";
public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java
new file mode 100644
index 000000000000..be38c259d00d
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class NonResizeableFixedAspectRatioPortraitActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.activity_non_resizeable);
+ }
+}
diff --git a/tests/graphics/SilkFX/res/layout/view_blur_behind.xml b/tests/graphics/SilkFX/res/layout/view_blur_behind.xml
new file mode 100644
index 000000000000..83b1fa4b73cb
--- /dev/null
+++ b/tests/graphics/SilkFX/res/layout/view_blur_behind.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="wowwowwowwowwowwowwowwowwowwowwowwowwowwowwow" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="I'm a little teapot" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="Something. Something." />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="/\\/\\/\\/\\/\\/\\/\\/\\/\\/" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="wowwowwowwowwowwowwowwowwowwowwowwowwowwowwow" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="I'm a little teapot" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="Something. Something." />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="/\\/\\/\\/\\/\\/\\/\\/\\/\\/" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:textSize="24dp"
+ android:text="^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^" />
+
+ </LinearLayout>
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="300dp" />
+
+ <com.android.test.silkfx.materials.BlurBehindContainer
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#33AAAAAA"
+ android:padding="32dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="48dp"
+ android:text="Blur!" />
+
+ </com.android.test.silkfx.materials.BlurBehindContainer>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1024dp" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+</FrameLayout> \ No newline at end of file
diff --git a/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt b/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt
index 59a6078376cf..6b6d3b8d3d12 100644
--- a/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt
+++ b/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt
@@ -61,7 +61,8 @@ private val AllDemos = listOf(
)),
DemoGroup("Materials", listOf(
Demo("Glass", GlassActivity::class),
- Demo("Background Blur", BackgroundBlurActivity::class)
+ Demo("Background Blur", BackgroundBlurActivity::class),
+ Demo("View blur behind", R.layout.view_blur_behind, commonControls = false)
))
)
diff --git a/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt b/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt
new file mode 100644
index 000000000000..ce6348e32969
--- /dev/null
+++ b/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test.silkfx.materials
+
+import android.content.Context
+import android.graphics.RenderEffect
+import android.graphics.Shader
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+class BlurBehindContainer(context: Context, attributeSet: AttributeSet) : FrameLayout(context, attributeSet) {
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ setBackdropRenderEffect(
+ RenderEffect.createBlurEffect(16.0f, 16.0f, Shader.TileMode.CLAMP))
+ }
+} \ No newline at end of file
diff --git a/tests/testables/Android.bp b/tests/testables/Android.bp
index f2111856c666..17cc0b2a5884 100644
--- a/tests/testables/Android.bp
+++ b/tests/testables/Android.bp
@@ -35,4 +35,8 @@ java_library {
"androidx.test.rules",
"mockito-target-inline-minus-junit4",
],
+ static_libs: [
+ "PlatformMotionTesting",
+ "kotlinx_coroutines_test",
+ ],
}
diff --git a/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt
new file mode 100644
index 000000000000..b27b8269575b
--- /dev/null
+++ b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation
+
+import android.animation.AnimatorTestRuleToolkit.Companion.TAG
+import android.util.Log
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import platform.test.motion.MotionTestRule
+import platform.test.motion.RecordedMotion
+import platform.test.motion.RecordedMotion.Companion.create
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.Feature
+import platform.test.motion.golden.FrameId
+import platform.test.motion.golden.TimeSeries
+import platform.test.motion.golden.TimeSeriesCaptureScope
+import platform.test.motion.golden.TimestampFrameId
+
+class AnimatorTestRuleToolkit(val animatorTestRule: AnimatorTestRule, val testScope: TestScope) {
+ internal companion object {
+ const val TAG = "AnimatorRuleToolkit"
+ }
+}
+
+/**
+ * Controls the timing of the motion recording.
+ *
+ * The time series is recorded while the [recording] function is running.
+ */
+class MotionControl(val recording: MotionControlFn)
+
+typealias MotionControlFn = suspend MotionControlScope.() -> Unit
+
+interface MotionControlScope {
+ /** Waits until [check] returns true. Invoked on each frame. */
+ suspend fun awaitCondition(check: () -> Boolean)
+
+ /** Waits for [count] frames to be processed. */
+ suspend fun awaitFrames(count: Int = 1)
+}
+
+/** Defines the sampling of features during a test run. */
+data class AnimatorRuleRecordingSpec<T>(
+ /** The root `observing` object, available in [timeSeriesCapture]'s [TimeSeriesCaptureScope]. */
+ val captureRoot: T,
+
+ /** The timing for the recording. */
+ val motionControl: MotionControl,
+
+ /** Time interval between frame captures, in milliseconds. */
+ val frameDurationMs: Long = 16L,
+
+ /** Produces the time-series, invoked on each animation frame. */
+ val timeSeriesCapture: TimeSeriesCaptureScope<T>.() -> Unit,
+)
+
+/** Records the time-series of the features specified in [recordingSpec]. */
+fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion(
+ recordingSpec: AnimatorRuleRecordingSpec<T>,
+): RecordedMotion {
+ with(toolkit.animatorTestRule) {
+ val frameIdCollector = mutableListOf<FrameId>()
+ val propertyCollector = mutableMapOf<String, MutableList<DataPoint<*>>>()
+
+ fun recordFrame(frameId: FrameId) {
+ Log.i(TAG, "recordFrame($frameId)")
+ frameIdCollector.add(frameId)
+ recordingSpec.timeSeriesCapture.invoke(
+ TimeSeriesCaptureScope(recordingSpec.captureRoot, propertyCollector)
+ )
+ }
+
+ val motionControl =
+ MotionControlImpl(
+ toolkit.animatorTestRule,
+ toolkit.testScope,
+ recordingSpec.frameDurationMs,
+ recordingSpec.motionControl,
+ )
+
+ Log.i(TAG, "recordMotion() begin recording")
+
+ val startFrameTime = currentTime
+ while (!motionControl.recordingEnded) {
+ recordFrame(TimestampFrameId(currentTime - startFrameTime))
+ motionControl.nextFrame()
+ }
+
+ Log.i(TAG, "recordMotion() end recording")
+
+ val timeSeries =
+ TimeSeries(
+ frameIdCollector.toList(),
+ propertyCollector.entries.map { entry -> Feature(entry.key, entry.value) },
+ )
+
+ return create(timeSeries, null)
+ }
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+private class MotionControlImpl(
+ val animatorTestRule: AnimatorTestRule,
+ val testScope: TestScope,
+ val frameMs: Long,
+ motionControl: MotionControl,
+) : MotionControlScope {
+ private val recordingJob = motionControl.recording.launch()
+
+ private val frameEmitter = MutableStateFlow<Long>(0)
+ private val onFrame = frameEmitter.asStateFlow()
+
+ var recordingEnded: Boolean = false
+
+ fun nextFrame() {
+ animatorTestRule.advanceTimeBy(frameMs)
+
+ frameEmitter.tryEmit(animatorTestRule.currentTime)
+ testScope.runCurrent()
+
+ if (recordingJob.isCompleted) {
+ recordingEnded = true
+ }
+ }
+
+ override suspend fun awaitCondition(check: () -> Boolean) {
+ onFrame.takeWhile { !check() }.collect {}
+ }
+
+ override suspend fun awaitFrames(count: Int) {
+ onFrame.take(count).collect {}
+ }
+
+ private fun MotionControlFn.launch(): Job {
+ val function = this
+ return testScope.launch { function() }
+ }
+}
diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp
index c23f41a6c3d4..71105646a3d3 100644
--- a/tests/testables/tests/Android.bp
+++ b/tests/testables/tests/Android.bp
@@ -29,13 +29,17 @@ android_test {
"src/**/*.kt",
"src/**/I*.aidl",
],
+ asset_dirs: ["goldens"],
resource_dirs: ["res"],
static_libs: [
+ "PlatformMotionTesting",
"androidx.core_core-animation",
"androidx.core_core-ktx",
+ "androidx.test.ext.junit",
"androidx.test.rules",
"androidx.test.ext.junit",
"hamcrest-library",
+ "kotlinx_coroutines_test",
"mockito-target-inline-minus-junit4",
"testables",
"truth",
diff --git a/tests/testables/tests/goldens/recordMotion_withAnimator.json b/tests/testables/tests/goldens/recordMotion_withAnimator.json
new file mode 100644
index 000000000000..87fece5395e7
--- /dev/null
+++ b/tests/testables/tests/goldens/recordMotion_withAnimator.json
@@ -0,0 +1,64 @@
+{
+ "frame_ids": [
+ 0,
+ 20,
+ 40,
+ 60,
+ 80,
+ 100,
+ 120,
+ 140,
+ 160,
+ 180,
+ 200,
+ 220,
+ 240,
+ 260,
+ 280,
+ 300,
+ 320,
+ 340,
+ 360,
+ 380,
+ 400,
+ 420,
+ 440,
+ 460,
+ 480,
+ 500
+ ],
+ "features": [
+ {
+ "name": "value",
+ "type": "float",
+ "data_points": [
+ 1,
+ 0.9960574,
+ 0.98429155,
+ 0.9648882,
+ 0.9381534,
+ 0.9045085,
+ 0.8644843,
+ 0.818712,
+ 0.76791346,
+ 0.7128896,
+ 0.65450853,
+ 0.5936906,
+ 0.5313952,
+ 0.46860474,
+ 0.40630943,
+ 0.34549147,
+ 0.2871104,
+ 0.23208654,
+ 0.181288,
+ 0.13551569,
+ 0.09549153,
+ 0.061846733,
+ 0.035111785,
+ 0.015708387,
+ 0.003942609,
+ 0
+ ]
+ }
+ ]
+}
diff --git a/tests/testables/tests/goldens/recordMotion_withSpring.json b/tests/testables/tests/goldens/recordMotion_withSpring.json
new file mode 100644
index 000000000000..e9fb5b4af869
--- /dev/null
+++ b/tests/testables/tests/goldens/recordMotion_withSpring.json
@@ -0,0 +1,48 @@
+{
+ "frame_ids": [
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272
+ ],
+ "features": [
+ {
+ "name": "value",
+ "type": "float",
+ "data_points": [
+ 1,
+ 0.9488604,
+ 0.83574325,
+ 0.7016156,
+ 0.5691678,
+ 0.4497436,
+ 0.34789434,
+ 0.26431116,
+ 0.19766562,
+ 0.14572789,
+ 0.10601636,
+ 0.076149896,
+ 0.05401709,
+ 0.037837274,
+ 0.026161024,
+ 0.017839976,
+ 0.011983856,
+ 0.007914998
+ ]
+ }
+ ]
+}
diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt
new file mode 100644
index 000000000000..fbef4899bca9
--- /dev/null
+++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation
+
+import android.util.FloatProperty
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.internal.dynamicanimation.animation.SpringAnimation
+import com.android.internal.dynamicanimation.animation.SpringForce
+import kotlinx.coroutines.test.TestScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.motion.MotionTestRule
+import platform.test.motion.RecordedMotion
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.asDataPoint
+import platform.test.motion.testing.createGoldenPathManager
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AnimatorTestRuleToolkitTest {
+ companion object {
+ private val GOLDEN_PATH_MANAGER =
+ createGoldenPathManager("frameworks/base/tests/testables/tests/goldens")
+
+ private val TEST_PROPERTY =
+ object : FloatProperty<TestState>("value") {
+ override fun get(state: TestState): Float {
+ return state.animatedValue
+ }
+
+ override fun setValue(state: TestState, value: Float) {
+ state.animatedValue = value
+ }
+ }
+ }
+
+ @get:Rule(order = 0) val animatorTestRule = AnimatorTestRule(this)
+ @get:Rule(order = 1)
+ val motionRule =
+ MotionTestRule(AnimatorTestRuleToolkit(animatorTestRule, TestScope()), GOLDEN_PATH_MANAGER)
+
+ @Test
+ fun recordMotion_withAnimator() {
+ val state = TestState()
+ AnimatorSet().apply {
+ duration = 500
+ play(
+ ValueAnimator.ofFloat(state.animatedValue, 0f).apply {
+ addUpdateListener { state.animatedValue = it.animatedValue as Float }
+ }
+ )
+ getInstrumentation().runOnMainSync { start() }
+ }
+
+ val recordedMotion =
+ record(state, MotionControl { awaitFrames(count = 26) }, sampleIntervalMs = 20L)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordMotion_withAnimator")
+ }
+
+ @Test
+ fun recordMotion_withSpring() {
+ val state = TestState()
+ var isDone = false
+ SpringAnimation(state, TEST_PROPERTY).apply {
+ spring =
+ SpringForce(0f).apply {
+ stiffness = 500f
+ dampingRatio = 0.95f
+ }
+
+ setStartValue(1f)
+ setMinValue(0f)
+ setMaxValue(1f)
+ minimumVisibleChange = 0.01f
+
+ addEndListener { _, _, _, _ -> isDone = true }
+ getInstrumentation().runOnMainSync { start() }
+ }
+
+ val recordedMotion =
+ record(state, MotionControl { awaitCondition { isDone } }, sampleIntervalMs = 16L)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordMotion_withSpring")
+ }
+
+ private fun record(
+ state: TestState,
+ motionControl: MotionControl,
+ sampleIntervalMs: Long,
+ ): RecordedMotion {
+ var recordedMotion: RecordedMotion? = null
+ getInstrumentation().runOnMainSync {
+ recordedMotion =
+ motionRule.recordMotion(
+ AnimatorRuleRecordingSpec(
+ state,
+ motionControl,
+ sampleIntervalMs,
+ ) {
+ feature(
+ FeatureCapture("value") { state -> state.animatedValue.asDataPoint() },
+ "value",
+ )
+ }
+ )
+ }
+ return recordedMotion!!
+ }
+
+ data class TestState(var animatedValue: Float = 1f)
+}