Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bb3e034b authored by Jeff DeCew's avatar Jeff DeCew Committed by Android (Google) Code Review
Browse files

Merge "Add AnimatorTestRule for use with platform animators" into udc-qpr-dev

parents ff40cef9 70583899
Loading
Loading
Loading
Loading
+20 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.animation;

import android.annotation.Nullable;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.ArrayMap;
@@ -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.
@@ -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;
        }
+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
    }
}
+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)
    }
}
+14 −1
Original line number Diff line number Diff line
@@ -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 }
@@ -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 }
@@ -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()
    }

+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