Loading core/java/android/animation/AnimationHandler.java +20 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package android.animation; import android.annotation.Nullable; import android.os.SystemClock; import android.os.SystemProperties; import android.util.ArrayMap; Loading Loading @@ -91,15 +92,30 @@ public class AnimationHandler { }; public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>(); private static AnimationHandler sTestHandler = null; private boolean mListDirty = false; public static AnimationHandler getInstance() { if (sTestHandler != null) { return sTestHandler; } if (sAnimatorHandler.get() == null) { sAnimatorHandler.set(new AnimationHandler()); } return sAnimatorHandler.get(); } /** * Sets an instance that will be returned by {@link #getInstance()} on every thread. * @return the previously active test handler, if any. * @hide */ public static @Nullable AnimationHandler setTestHandler(@Nullable AnimationHandler handler) { AnimationHandler oldHandler = sTestHandler; sTestHandler = handler; return oldHandler; } /** * System property that controls the behavior of pausing infinite animators when an app * is moved to the background. Loading Loading @@ -369,7 +385,10 @@ public class AnimationHandler { * Return the number of callbacks that have registered for frame callbacks. */ public static int getAnimationCount() { AnimationHandler handler = sAnimatorHandler.get(); AnimationHandler handler = sTestHandler; if (handler == null) { handler = sAnimatorHandler.get(); } if (handler == null) { return 0; } Loading packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt 0 → 100644 +90 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.core.animation.doOnEnd import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith /** * This test class validates that two tests' animators are isolated from each other when using the * same animator test rule. This is a test to prevent future instances of b/275602127. */ @RunWith(AndroidTestingRunner::class) @SmallTest @RunWithLooper(setAsMainLooper = true) class AnimatorTestRuleIsolationTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() @Test fun testA() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchA at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchA = true } start() } ObjectAnimator.ofFloat(0f, 1f).apply { duration = 150 doOnEnd { didTouchA = true } start() } // WHEN when you advance time so that only one of the animations has ended animatorTestRule.advanceTimeBy(100) // VERIFY we did indeed end the current animation assertThat(didTouchA).isTrue() // VERIFY advancing the animator did NOT cause testB's animator to end assertThat(didTouchB).isFalse() } @Test fun testB() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchB at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchB = true } start() } ObjectAnimator.ofFloat(0f, 1f).apply { duration = 150 doOnEnd { didTouchB = true } start() } animatorTestRule.advanceTimeBy(100) // VERIFY advancing the animator did NOT cause testA's animator to end assertThat(didTouchA).isFalse() // VERIFY we did indeed end the current animation assertThat(didTouchB).isTrue() } companion object { var didTouchA = false var didTouchB = false } } packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt 0 → 100644 +193 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.core.animation.doOnEnd import androidx.test.filters.SmallTest import com.android.app.animation.Interpolators import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @SmallTest @RunWithLooper(setAsMainLooper = true) class AnimatorTestRulePrecisionTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() var value1: Float = -1f var value2: Float = -1f private inline fun animateThis( propertyName: String, duration: Long, startDelay: Long = 0, crossinline onEndAction: (animator: Animator) -> Unit, ) { ObjectAnimator.ofFloat(this, propertyName, 0f, 1f).also { it.interpolator = Interpolators.LINEAR it.duration = duration it.startDelay = startDelay it.doOnEnd(onEndAction) it.start() } } @Test fun testSingleAnimator() { var ended = false animateThis("value1", duration = 100) { ended = true } assertThat(value1).isEqualTo(0f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(50) assertThat(value1).isEqualTo(0.5f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(49) assertThat(value1).isEqualTo(0.99f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(ended).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } @Test fun testDelayedAnimator() { var ended = false animateThis("value1", duration = 100, startDelay = 50) { ended = true } assertThat(value1).isEqualTo(-1f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(49) assertThat(value1).isEqualTo(-1f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(0f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(0.99f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(ended).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } @Test fun testTwoAnimators() { var ended1 = false var ended2 = false animateThis("value1", duration = 100) { ended1 = true } animateThis("value2", duration = 200) { ended2 = true } assertThat(value1).isEqualTo(0f) assertThat(value2).isEqualTo(0f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(0.99f) assertThat(value2).isEqualTo(0.495f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0.5f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0.995f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(1f) assertThat(ended1).isTrue() assertThat(ended2).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } @Test fun testChainedAnimators() { var ended1 = false var ended2 = false animateThis("value1", duration = 100) { ended1 = true animateThis("value2", duration = 100) { ended2 = true } } assertThat(value1).isEqualTo(0f) assertThat(value2).isEqualTo(-1f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(0.99f) assertThat(value2).isEqualTo(-1f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0.99f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(1f) assertThat(ended1).isTrue() assertThat(ended2).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } } packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt→packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt +14 −1 Original line number Diff line number Diff line Loading @@ -25,17 +25,23 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith /** * This test class validates that two tests' animators are isolated from each other when using the * same animator test rule. This is a test to prevent future instances of b/275602127. */ @RunWith(AndroidTestingRunner::class) @SmallTest @RunWithLooper(setAsMainLooper = true) class AnimatorTestRuleTest : SysuiTestCase() { class AnimatorTestRuleIsolationTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() @Test fun testA() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchA at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchA = true } Loading @@ -46,15 +52,20 @@ class AnimatorTestRuleTest : SysuiTestCase() { doOnEnd { didTouchA = true } start() } // WHEN when you advance time so that only one of the animations has ended animatorTestRule.advanceTimeBy(100) // VERIFY we did indeed end the current animation assertThat(didTouchA).isTrue() // VERIFY advancing the animator did NOT cause testB's animator to end assertThat(didTouchB).isFalse() } @Test fun testB() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchB at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchB = true } Loading @@ -66,7 +77,9 @@ class AnimatorTestRuleTest : SysuiTestCase() { start() } animatorTestRule.advanceTimeBy(100) // VERIFY advancing the animator did NOT cause testA's animator to end assertThat(didTouchA).isFalse() // VERIFY we did indeed end the current animation assertThat(didTouchB).isTrue() } Loading packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt 0 → 100644 +111 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.os.Looper import android.util.Log import com.android.systemui.util.test.TestExceptionDeferrer import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement /** * This rule is intended to be used by System UI tests that are otherwise blocked from using * animators because of [PlatformAnimatorIsolationRule]. It is preferred that test authors use * [AnimatorTestRule], as that rule allows test authors to step through animations and removes the * need for tests to handle multiple threads. However, many System UI tests were written before this * was conceivable, so this rule is intended to support those legacy tests. */ class AnimatorIsolationWorkaroundRule( private val requiredLooper: Looper? = Looper.getMainLooper(), ) : TestRule { private inner class IsolationWorkaroundHandler(ruleThread: Thread) : AnimationHandler() { private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread) private val addedCallbacks = mutableSetOf<AnimationFrameCallback>() fun tearDownAndThrowDeferred() { addedCallbacks.forEach { super.removeCallback(it) } exceptionDeferrer.throwDeferred() } override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) { checkLooper() if (callback != null) { addedCallbacks.add(callback) } super.addAnimationFrameCallback(callback, delay) } override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) { checkLooper() super.addOneShotCommitCallback(callback) } override fun removeCallback(callback: AnimationFrameCallback?) { super.removeCallback(callback) } override fun setProvider(provider: AnimationFrameCallbackProvider?) { checkLooper() super.setProvider(provider) } override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) { checkLooper() super.autoCancelBasedOn(objectAnimator) } private fun checkLooper() { exceptionDeferrer.check(requiredLooper == null || Looper.myLooper() == requiredLooper) { "Animations are being registered on a different looper than the expected one!" + " expected=$requiredLooper actual=${Looper.myLooper()}" } } } override fun apply(base: Statement, description: Description): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { val workaroundHandler = IsolationWorkaroundHandler(Thread.currentThread()) val prevInstance = AnimationHandler.setTestHandler(workaroundHandler) check(PlatformAnimatorIsolationRule.isIsolatingHandler(prevInstance)) { "AnimatorIsolationWorkaroundRule must be used within " + "PlatformAnimatorIsolationRule, but test handler was $prevInstance" } try { base.evaluate() val count = AnimationHandler.getAnimationCount() if (count > 0) { Log.w(TAG, "Animations still running: $count") } } finally { val handlerAtEnd = AnimationHandler.setTestHandler(prevInstance) check(workaroundHandler == handlerAtEnd) { "Test handler was altered: expected=$workaroundHandler actual=$handlerAtEnd" } // Pass or fail, errors caught here should be the reason the test fails workaroundHandler.tearDownAndThrowDeferred() } } } } private companion object { private const val TAG = "AnimatorIsolationWorkaroundRule" } } Loading
core/java/android/animation/AnimationHandler.java +20 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package android.animation; import android.annotation.Nullable; import android.os.SystemClock; import android.os.SystemProperties; import android.util.ArrayMap; Loading Loading @@ -91,15 +92,30 @@ public class AnimationHandler { }; public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>(); private static AnimationHandler sTestHandler = null; private boolean mListDirty = false; public static AnimationHandler getInstance() { if (sTestHandler != null) { return sTestHandler; } if (sAnimatorHandler.get() == null) { sAnimatorHandler.set(new AnimationHandler()); } return sAnimatorHandler.get(); } /** * Sets an instance that will be returned by {@link #getInstance()} on every thread. * @return the previously active test handler, if any. * @hide */ public static @Nullable AnimationHandler setTestHandler(@Nullable AnimationHandler handler) { AnimationHandler oldHandler = sTestHandler; sTestHandler = handler; return oldHandler; } /** * System property that controls the behavior of pausing infinite animators when an app * is moved to the background. Loading Loading @@ -369,7 +385,10 @@ public class AnimationHandler { * Return the number of callbacks that have registered for frame callbacks. */ public static int getAnimationCount() { AnimationHandler handler = sAnimatorHandler.get(); AnimationHandler handler = sTestHandler; if (handler == null) { handler = sAnimatorHandler.get(); } if (handler == null) { return 0; } Loading
packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt 0 → 100644 +90 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.core.animation.doOnEnd import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith /** * This test class validates that two tests' animators are isolated from each other when using the * same animator test rule. This is a test to prevent future instances of b/275602127. */ @RunWith(AndroidTestingRunner::class) @SmallTest @RunWithLooper(setAsMainLooper = true) class AnimatorTestRuleIsolationTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() @Test fun testA() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchA at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchA = true } start() } ObjectAnimator.ofFloat(0f, 1f).apply { duration = 150 doOnEnd { didTouchA = true } start() } // WHEN when you advance time so that only one of the animations has ended animatorTestRule.advanceTimeBy(100) // VERIFY we did indeed end the current animation assertThat(didTouchA).isTrue() // VERIFY advancing the animator did NOT cause testB's animator to end assertThat(didTouchB).isFalse() } @Test fun testB() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchB at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchB = true } start() } ObjectAnimator.ofFloat(0f, 1f).apply { duration = 150 doOnEnd { didTouchB = true } start() } animatorTestRule.advanceTimeBy(100) // VERIFY advancing the animator did NOT cause testA's animator to end assertThat(didTouchA).isFalse() // VERIFY we did indeed end the current animation assertThat(didTouchB).isTrue() } companion object { var didTouchA = false var didTouchB = false } }
packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt 0 → 100644 +193 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.core.animation.doOnEnd import androidx.test.filters.SmallTest import com.android.app.animation.Interpolators import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @SmallTest @RunWithLooper(setAsMainLooper = true) class AnimatorTestRulePrecisionTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() var value1: Float = -1f var value2: Float = -1f private inline fun animateThis( propertyName: String, duration: Long, startDelay: Long = 0, crossinline onEndAction: (animator: Animator) -> Unit, ) { ObjectAnimator.ofFloat(this, propertyName, 0f, 1f).also { it.interpolator = Interpolators.LINEAR it.duration = duration it.startDelay = startDelay it.doOnEnd(onEndAction) it.start() } } @Test fun testSingleAnimator() { var ended = false animateThis("value1", duration = 100) { ended = true } assertThat(value1).isEqualTo(0f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(50) assertThat(value1).isEqualTo(0.5f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(49) assertThat(value1).isEqualTo(0.99f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(ended).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } @Test fun testDelayedAnimator() { var ended = false animateThis("value1", duration = 100, startDelay = 50) { ended = true } assertThat(value1).isEqualTo(-1f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(49) assertThat(value1).isEqualTo(-1f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(0f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(0.99f) assertThat(ended).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(ended).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } @Test fun testTwoAnimators() { var ended1 = false var ended2 = false animateThis("value1", duration = 100) { ended1 = true } animateThis("value2", duration = 200) { ended2 = true } assertThat(value1).isEqualTo(0f) assertThat(value2).isEqualTo(0f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(0.99f) assertThat(value2).isEqualTo(0.495f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0.5f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0.995f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(1f) assertThat(ended1).isTrue() assertThat(ended2).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } @Test fun testChainedAnimators() { var ended1 = false var ended2 = false animateThis("value1", duration = 100) { ended1 = true animateThis("value2", duration = 100) { ended2 = true } } assertThat(value1).isEqualTo(0f) assertThat(value2).isEqualTo(-1f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(0.99f) assertThat(value2).isEqualTo(-1f) assertThat(ended1).isFalse() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(99) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(0.99f) assertThat(ended1).isTrue() assertThat(ended2).isFalse() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) animatorTestRule.advanceTimeBy(1) assertThat(value1).isEqualTo(1f) assertThat(value2).isEqualTo(1f) assertThat(ended1).isTrue() assertThat(ended2).isTrue() assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) } }
packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt→packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt +14 −1 Original line number Diff line number Diff line Loading @@ -25,17 +25,23 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith /** * This test class validates that two tests' animators are isolated from each other when using the * same animator test rule. This is a test to prevent future instances of b/275602127. */ @RunWith(AndroidTestingRunner::class) @SmallTest @RunWithLooper(setAsMainLooper = true) class AnimatorTestRuleTest : SysuiTestCase() { class AnimatorTestRuleIsolationTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() @Test fun testA() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchA at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchA = true } Loading @@ -46,15 +52,20 @@ class AnimatorTestRuleTest : SysuiTestCase() { doOnEnd { didTouchA = true } start() } // WHEN when you advance time so that only one of the animations has ended animatorTestRule.advanceTimeBy(100) // VERIFY we did indeed end the current animation assertThat(didTouchA).isTrue() // VERIFY advancing the animator did NOT cause testB's animator to end assertThat(didTouchB).isFalse() } @Test fun testB() { // GIVEN global state is reset at the start of the test didTouchA = false didTouchB = false // WHEN starting 2 animations of different durations, and setting didTouchB at the end ObjectAnimator.ofFloat(0f, 1f).apply { duration = 100 doOnEnd { didTouchB = true } Loading @@ -66,7 +77,9 @@ class AnimatorTestRuleTest : SysuiTestCase() { start() } animatorTestRule.advanceTimeBy(100) // VERIFY advancing the animator did NOT cause testA's animator to end assertThat(didTouchA).isFalse() // VERIFY we did indeed end the current animation assertThat(didTouchB).isTrue() } Loading
packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt 0 → 100644 +111 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.os.Looper import android.util.Log import com.android.systemui.util.test.TestExceptionDeferrer import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement /** * This rule is intended to be used by System UI tests that are otherwise blocked from using * animators because of [PlatformAnimatorIsolationRule]. It is preferred that test authors use * [AnimatorTestRule], as that rule allows test authors to step through animations and removes the * need for tests to handle multiple threads. However, many System UI tests were written before this * was conceivable, so this rule is intended to support those legacy tests. */ class AnimatorIsolationWorkaroundRule( private val requiredLooper: Looper? = Looper.getMainLooper(), ) : TestRule { private inner class IsolationWorkaroundHandler(ruleThread: Thread) : AnimationHandler() { private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread) private val addedCallbacks = mutableSetOf<AnimationFrameCallback>() fun tearDownAndThrowDeferred() { addedCallbacks.forEach { super.removeCallback(it) } exceptionDeferrer.throwDeferred() } override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) { checkLooper() if (callback != null) { addedCallbacks.add(callback) } super.addAnimationFrameCallback(callback, delay) } override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) { checkLooper() super.addOneShotCommitCallback(callback) } override fun removeCallback(callback: AnimationFrameCallback?) { super.removeCallback(callback) } override fun setProvider(provider: AnimationFrameCallbackProvider?) { checkLooper() super.setProvider(provider) } override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) { checkLooper() super.autoCancelBasedOn(objectAnimator) } private fun checkLooper() { exceptionDeferrer.check(requiredLooper == null || Looper.myLooper() == requiredLooper) { "Animations are being registered on a different looper than the expected one!" + " expected=$requiredLooper actual=${Looper.myLooper()}" } } } override fun apply(base: Statement, description: Description): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { val workaroundHandler = IsolationWorkaroundHandler(Thread.currentThread()) val prevInstance = AnimationHandler.setTestHandler(workaroundHandler) check(PlatformAnimatorIsolationRule.isIsolatingHandler(prevInstance)) { "AnimatorIsolationWorkaroundRule must be used within " + "PlatformAnimatorIsolationRule, but test handler was $prevInstance" } try { base.evaluate() val count = AnimationHandler.getAnimationCount() if (count > 0) { Log.w(TAG, "Animations still running: $count") } } finally { val handlerAtEnd = AnimationHandler.setTestHandler(prevInstance) check(workaroundHandler == handlerAtEnd) { "Test handler was altered: expected=$workaroundHandler actual=$handlerAtEnd" } // Pass or fail, errors caught here should be the reason the test fails workaroundHandler.tearDownAndThrowDeferred() } } } } private companion object { private const val TAG = "AnimatorIsolationWorkaroundRule" } }