Loading packages/SystemUI/multivalentTests/src/com/android/systemui/util/animation/ScreenOffAnimationGuardKtTest.kt→packages/SystemUI/multivalentTests/src/com/android/systemui/util/animation/ScreenOffAnimationGuardTest.kt +50 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.util.animation import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorTestRule import android.animation.ValueAnimator import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags Loading @@ -30,15 +31,18 @@ import com.android.systemui.log.assertLogsWtfs import com.android.systemui.runOnMainThreadAndWaitForIdleSync import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class ScreenOffAnimationGuardKtTest : SysuiTestCase() { class ScreenOffAnimationGuardTest : SysuiTestCase() { private val kosmos = Kosmos() @get:Rule public val animatorTestRuleX = androidx.core.animation.AnimatorTestRule() @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuard_screenOn_allGood() { Loading @@ -46,7 +50,7 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ false }) valueAnimator.duration = 10 valueAnimator.duration = 100 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { Loading @@ -67,7 +71,7 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.duration = 100 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { Loading @@ -82,6 +86,48 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { } } @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuardX_screenOn_allGood() { val valueAnimator = androidx.core.animation.ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ false }) valueAnimator.duration = 100 valueAnimator.addListener( object : androidx.core.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: androidx.core.animation.Animator) { latch.countDown() } } ) runOnMainThreadAndWaitForIdleSync { valueAnimator.start() } latch.await(1, TimeUnit.SECONDS) // No Log.WTF } @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuardX_screenOff_reportsWtf() { val valueAnimator = androidx.core.animation.ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 100 valueAnimator.addListener( object : androidx.core.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: androidx.core.animation.Animator) { latch.countDown() } } ) assertLogsWtfs { runOnMainThreadAndWaitForIdleSync { valueAnimator.start() } latch.await(1, TimeUnit.SECONDS) } } @Test @DisableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuard_screenOff_flagDisabled_doesNotReportWtf() { Loading @@ -89,7 +135,7 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.duration = 100 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { Loading packages/SystemUI/src/com/android/systemui/util/animation/ScreenOffAnimationGuard.kt +96 −10 Original line number Diff line number Diff line Loading @@ -26,8 +26,10 @@ import android.view.Display import android.view.View import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieDrawable import com.android.app.tracing.traceSection import com.android.systemui.Flags.screenOffAnimationGuardEnabled import com.android.systemui.res.R import com.android.systemui.util.weakReference import java.lang.ref.WeakReference private const val LOG_TAG = "AnimationGuard" Loading Loading @@ -95,7 +97,8 @@ fun ValueAnimator.enableScreenOffAnimationGuard(context: Context) { return } enableScreenOffAnimationGuard({ context.display.state == Display.STATE_OFF }) val weakContext by weakReference(context) enableScreenOffAnimationGuard({ weakContext?.display?.state == Display.STATE_OFF }) } /** Attaches an animation guard listener to the given ValueAnimator. */ Loading @@ -109,38 +112,121 @@ fun ValueAnimator.enableScreenOffAnimationGuard(isDisplayOffPredicate: () -> Boo this.addUpdateListener(listener) } /** * Attaches a listener which will report a [Log.wtf] error if the animator is attempting to render * frames while the screen is off. */ fun androidx.core.animation.ValueAnimator.enableScreenOffAnimationGuard(context: Context) { if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { return } val weakContext by weakReference(context) enableScreenOffAnimationGuard({ weakContext?.display?.state == Display.STATE_OFF }) } /** Attaches an animation guard listener to the given ValueAnimator. */ fun androidx.core.animation.ValueAnimator.enableScreenOffAnimationGuard( isDisplayOffPredicate: () -> Boolean ) { if (!screenOffAnimationGuardEnabled()) { return } val listener = ScreenOffAnimationGuardListener(isDisplayOffPredicate) this.addListener(listener) this.addUpdateListener(listener) } /** * Remembers the stack trace of started animation and then reports an error if it runs when screen * is off. * * The implementation is a bit wonky because we account for both platform and AndroidX versions of * the listener. */ private class ScreenOffAnimationGuardListener(private val isDisplayOffPredicate: () -> Boolean) : Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener, androidx.core.animation.Animator.AnimatorListener, androidx.core.animation.Animator.AnimatorUpdateListener { val FRAMES_WHEN_SCREEN_OFF_LIMIT = 2 // Holds the exception stack trace for the report. var animationStartedStackTrace: Exception? = null var framesRenderedWhileScreenIsOff = 0 var animationDuringScreenOffReported = false /* Start animation, platform version. */ override fun onAnimationStart(animation: Animator) { // This captures the stack trace of the starter of this animation. animationStartedStackTrace = AnimationDuringScreenOffException("Animation running during screen off.") animationDuringScreenOffReported = false captureAnimationStackTrace() } /* Start animation, AndroidX version. */ override fun onAnimationStart(animation: androidx.core.animation.Animator) { captureAnimationStackTrace() } /* End animation, platform version. */ override fun onAnimationEnd(animation: Animator) { animationStartedStackTrace = null } /* End animation, AndroidX version. */ override fun onAnimationEnd(animation: androidx.core.animation.Animator) { animationStartedStackTrace = null } /* Animation step, platform version. */ override fun onAnimationUpdate(animation: ValueAnimator) { if (!animationDuringScreenOffReported && isDisplayOffPredicate()) { Log.wtf(LOG_TAG, "View animator running during screen off.", animationStartedStackTrace) animationDuringScreenOffReported = true checkAndReportAnimationDuringScreenOff() } /* Animation step, AndroidX version. */ override fun onAnimationUpdate(animation: androidx.core.animation.Animator) { checkAndReportAnimationDuringScreenOff() } override fun onAnimationCancel(animation: Animator) {} override fun onAnimationCancel(animation: androidx.core.animation.Animator) {} override fun onAnimationRepeat(animation: Animator) {} override fun onAnimationRepeat(animation: androidx.core.animation.Animator) {} /** * Stores the stack trace of "startAnimation" caller so we can determine which animator is * actually running. */ fun captureAnimationStackTrace() = traceSection("captureAnimationStackTrace") { // This captures the stack trace of the starter of this animation. animationStartedStackTrace = AnimationDuringScreenOffException("Animation running during screen off.") } /** Reports WTF if we detect the animation running during screen off with saved stack trace. */ fun checkAndReportAnimationDuringScreenOff() { if (!animationDuringScreenOffReported && isDisplayOffPredicate()) { // We want to give a bit of a leeway to make sure we don't report on animators // that race against screen off - that is, animators that _just_ complete as // the screen turned off. framesRenderedWhileScreenIsOff++ if (framesRenderedWhileScreenIsOff < FRAMES_WHEN_SCREEN_OFF_LIMIT) { return } traceSection("reportAnimationDuringScreenOff") { Log.wtf( LOG_TAG, "View animator running during screen off.", animationStartedStackTrace, ) animationDuringScreenOffReported = true } } } } /** Used to record the stack trace of animation starter. */ Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/util/animation/ScreenOffAnimationGuardKtTest.kt→packages/SystemUI/multivalentTests/src/com/android/systemui/util/animation/ScreenOffAnimationGuardTest.kt +50 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.util.animation import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorTestRule import android.animation.ValueAnimator import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags Loading @@ -30,15 +31,18 @@ import com.android.systemui.log.assertLogsWtfs import com.android.systemui.runOnMainThreadAndWaitForIdleSync import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class ScreenOffAnimationGuardKtTest : SysuiTestCase() { class ScreenOffAnimationGuardTest : SysuiTestCase() { private val kosmos = Kosmos() @get:Rule public val animatorTestRuleX = androidx.core.animation.AnimatorTestRule() @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuard_screenOn_allGood() { Loading @@ -46,7 +50,7 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ false }) valueAnimator.duration = 10 valueAnimator.duration = 100 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { Loading @@ -67,7 +71,7 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.duration = 100 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { Loading @@ -82,6 +86,48 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { } } @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuardX_screenOn_allGood() { val valueAnimator = androidx.core.animation.ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ false }) valueAnimator.duration = 100 valueAnimator.addListener( object : androidx.core.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: androidx.core.animation.Animator) { latch.countDown() } } ) runOnMainThreadAndWaitForIdleSync { valueAnimator.start() } latch.await(1, TimeUnit.SECONDS) // No Log.WTF } @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuardX_screenOff_reportsWtf() { val valueAnimator = androidx.core.animation.ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 100 valueAnimator.addListener( object : androidx.core.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: androidx.core.animation.Animator) { latch.countDown() } } ) assertLogsWtfs { runOnMainThreadAndWaitForIdleSync { valueAnimator.start() } latch.await(1, TimeUnit.SECONDS) } } @Test @DisableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuard_screenOff_flagDisabled_doesNotReportWtf() { Loading @@ -89,7 +135,7 @@ class ScreenOffAnimationGuardKtTest : SysuiTestCase() { val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.duration = 100 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { Loading
packages/SystemUI/src/com/android/systemui/util/animation/ScreenOffAnimationGuard.kt +96 −10 Original line number Diff line number Diff line Loading @@ -26,8 +26,10 @@ import android.view.Display import android.view.View import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieDrawable import com.android.app.tracing.traceSection import com.android.systemui.Flags.screenOffAnimationGuardEnabled import com.android.systemui.res.R import com.android.systemui.util.weakReference import java.lang.ref.WeakReference private const val LOG_TAG = "AnimationGuard" Loading Loading @@ -95,7 +97,8 @@ fun ValueAnimator.enableScreenOffAnimationGuard(context: Context) { return } enableScreenOffAnimationGuard({ context.display.state == Display.STATE_OFF }) val weakContext by weakReference(context) enableScreenOffAnimationGuard({ weakContext?.display?.state == Display.STATE_OFF }) } /** Attaches an animation guard listener to the given ValueAnimator. */ Loading @@ -109,38 +112,121 @@ fun ValueAnimator.enableScreenOffAnimationGuard(isDisplayOffPredicate: () -> Boo this.addUpdateListener(listener) } /** * Attaches a listener which will report a [Log.wtf] error if the animator is attempting to render * frames while the screen is off. */ fun androidx.core.animation.ValueAnimator.enableScreenOffAnimationGuard(context: Context) { if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { return } val weakContext by weakReference(context) enableScreenOffAnimationGuard({ weakContext?.display?.state == Display.STATE_OFF }) } /** Attaches an animation guard listener to the given ValueAnimator. */ fun androidx.core.animation.ValueAnimator.enableScreenOffAnimationGuard( isDisplayOffPredicate: () -> Boolean ) { if (!screenOffAnimationGuardEnabled()) { return } val listener = ScreenOffAnimationGuardListener(isDisplayOffPredicate) this.addListener(listener) this.addUpdateListener(listener) } /** * Remembers the stack trace of started animation and then reports an error if it runs when screen * is off. * * The implementation is a bit wonky because we account for both platform and AndroidX versions of * the listener. */ private class ScreenOffAnimationGuardListener(private val isDisplayOffPredicate: () -> Boolean) : Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener, androidx.core.animation.Animator.AnimatorListener, androidx.core.animation.Animator.AnimatorUpdateListener { val FRAMES_WHEN_SCREEN_OFF_LIMIT = 2 // Holds the exception stack trace for the report. var animationStartedStackTrace: Exception? = null var framesRenderedWhileScreenIsOff = 0 var animationDuringScreenOffReported = false /* Start animation, platform version. */ override fun onAnimationStart(animation: Animator) { // This captures the stack trace of the starter of this animation. animationStartedStackTrace = AnimationDuringScreenOffException("Animation running during screen off.") animationDuringScreenOffReported = false captureAnimationStackTrace() } /* Start animation, AndroidX version. */ override fun onAnimationStart(animation: androidx.core.animation.Animator) { captureAnimationStackTrace() } /* End animation, platform version. */ override fun onAnimationEnd(animation: Animator) { animationStartedStackTrace = null } /* End animation, AndroidX version. */ override fun onAnimationEnd(animation: androidx.core.animation.Animator) { animationStartedStackTrace = null } /* Animation step, platform version. */ override fun onAnimationUpdate(animation: ValueAnimator) { if (!animationDuringScreenOffReported && isDisplayOffPredicate()) { Log.wtf(LOG_TAG, "View animator running during screen off.", animationStartedStackTrace) animationDuringScreenOffReported = true checkAndReportAnimationDuringScreenOff() } /* Animation step, AndroidX version. */ override fun onAnimationUpdate(animation: androidx.core.animation.Animator) { checkAndReportAnimationDuringScreenOff() } override fun onAnimationCancel(animation: Animator) {} override fun onAnimationCancel(animation: androidx.core.animation.Animator) {} override fun onAnimationRepeat(animation: Animator) {} override fun onAnimationRepeat(animation: androidx.core.animation.Animator) {} /** * Stores the stack trace of "startAnimation" caller so we can determine which animator is * actually running. */ fun captureAnimationStackTrace() = traceSection("captureAnimationStackTrace") { // This captures the stack trace of the starter of this animation. animationStartedStackTrace = AnimationDuringScreenOffException("Animation running during screen off.") } /** Reports WTF if we detect the animation running during screen off with saved stack trace. */ fun checkAndReportAnimationDuringScreenOff() { if (!animationDuringScreenOffReported && isDisplayOffPredicate()) { // We want to give a bit of a leeway to make sure we don't report on animators // that race against screen off - that is, animators that _just_ complete as // the screen turned off. framesRenderedWhileScreenIsOff++ if (framesRenderedWhileScreenIsOff < FRAMES_WHEN_SCREEN_OFF_LIMIT) { return } traceSection("reportAnimationDuringScreenOff") { Log.wtf( LOG_TAG, "View animator running during screen off.", animationStartedStackTrace, ) animationDuringScreenOffReported = true } } } } /** Used to record the stack trace of animation starter. */ Loading