Loading packages/SystemUI/multivalentTests/src/com/android/systemui/util/animation/ScreenOffAnimationGuardKtTest.kt 0 → 100644 +104 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.systemui.util.animation import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos import com.android.systemui.log.assertLogsWtfs import com.android.systemui.runOnMainThreadAndWaitForIdleSync import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class ScreenOffAnimationGuardKtTest : SysuiTestCase() { private val kosmos = Kosmos() @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuard_screenOn_allGood() { val valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ false }) valueAnimator.duration = 10 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(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 enableScreenOffAnimationGuard_screenOff_reportsWtf() { val valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(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() { val valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { latch.countDown() } } ) runOnMainThreadAndWaitForIdleSync { valueAnimator.start() } latch.await(1, TimeUnit.SECONDS) } } packages/SystemUI/src/com/android/systemui/util/animation/ScreenOffAnimationGuard.kt +64 −2 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package com.android.systemui.util.animation import android.animation.Animator import android.animation.ValueAnimator import android.content.Context import android.content.res.Resources import android.os.Build import android.util.Log Loading @@ -35,11 +37,11 @@ private const val LOG_TAG = "AnimationGuard" * screen is off. */ fun LottieAnimationView.enableScreenOffAnimationGuard() { if (!screenOffAnimationGuardEnabled()) { if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { return } if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { if (!screenOffAnimationGuardEnabled()) { return } Loading Loading @@ -83,3 +85,63 @@ fun LottieAnimationView.enableScreenOffAnimationGuard() { lottieDrawable.addAnimatorUpdateListener(screenOffListenerGuard) setTag(R.id.screen_off_animation_guard_set, System.identityHashCode(lottieDrawable)) } /** * Attaches a listener which will report a [Log.wtf] error if the animator is attempting to render * frames while the screen is off. */ fun ValueAnimator.enableScreenOffAnimationGuard(context: Context) { if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { return } enableScreenOffAnimationGuard({ context.display.state == Display.STATE_OFF }) } /** Attaches an animation guard listener to the given ValueAnimator. */ fun 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. */ private class ScreenOffAnimationGuardListener(private val isDisplayOffPredicate: () -> Boolean) : Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { // Holds the exception stack trace for the report. var animationStartedStackTrace: Exception? = null var animationDuringScreenOffReported = false 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 } override fun onAnimationEnd(animation: Animator) { animationStartedStackTrace = null } override fun onAnimationUpdate(animation: ValueAnimator) { if (!animationDuringScreenOffReported && isDisplayOffPredicate()) { Log.wtf(LOG_TAG, "View animator running during screen off.", animationStartedStackTrace) animationDuringScreenOffReported = true } } override fun onAnimationCancel(animation: Animator) {} override fun onAnimationRepeat(animation: Animator) {} } /** Used to record the stack trace of animation starter. */ private class AnimationDuringScreenOffException(message: String) : RuntimeException(message) Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/util/animation/ScreenOffAnimationGuardKtTest.kt 0 → 100644 +104 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.systemui.util.animation import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos import com.android.systemui.log.assertLogsWtfs import com.android.systemui.runOnMainThreadAndWaitForIdleSync import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class ScreenOffAnimationGuardKtTest : SysuiTestCase() { private val kosmos = Kosmos() @Test @EnableFlags(Flags.FLAG_SCREEN_OFF_ANIMATION_GUARD_ENABLED) fun enableScreenOffAnimationGuard_screenOn_allGood() { val valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ false }) valueAnimator.duration = 10 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(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 enableScreenOffAnimationGuard_screenOff_reportsWtf() { val valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(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() { val valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) val latch = CountDownLatch(1) valueAnimator.enableScreenOffAnimationGuard({ true }) valueAnimator.duration = 10 valueAnimator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { latch.countDown() } } ) runOnMainThreadAndWaitForIdleSync { valueAnimator.start() } latch.await(1, TimeUnit.SECONDS) } }
packages/SystemUI/src/com/android/systemui/util/animation/ScreenOffAnimationGuard.kt +64 −2 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package com.android.systemui.util.animation import android.animation.Animator import android.animation.ValueAnimator import android.content.Context import android.content.res.Resources import android.os.Build import android.util.Log Loading @@ -35,11 +37,11 @@ private const val LOG_TAG = "AnimationGuard" * screen is off. */ fun LottieAnimationView.enableScreenOffAnimationGuard() { if (!screenOffAnimationGuardEnabled()) { if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { return } if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { if (!screenOffAnimationGuardEnabled()) { return } Loading Loading @@ -83,3 +85,63 @@ fun LottieAnimationView.enableScreenOffAnimationGuard() { lottieDrawable.addAnimatorUpdateListener(screenOffListenerGuard) setTag(R.id.screen_off_animation_guard_set, System.identityHashCode(lottieDrawable)) } /** * Attaches a listener which will report a [Log.wtf] error if the animator is attempting to render * frames while the screen is off. */ fun ValueAnimator.enableScreenOffAnimationGuard(context: Context) { if (!(Build.IS_ENG || Build.IS_USERDEBUG)) { return } enableScreenOffAnimationGuard({ context.display.state == Display.STATE_OFF }) } /** Attaches an animation guard listener to the given ValueAnimator. */ fun 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. */ private class ScreenOffAnimationGuardListener(private val isDisplayOffPredicate: () -> Boolean) : Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { // Holds the exception stack trace for the report. var animationStartedStackTrace: Exception? = null var animationDuringScreenOffReported = false 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 } override fun onAnimationEnd(animation: Animator) { animationStartedStackTrace = null } override fun onAnimationUpdate(animation: ValueAnimator) { if (!animationDuringScreenOffReported && isDisplayOffPredicate()) { Log.wtf(LOG_TAG, "View animator running during screen off.", animationStartedStackTrace) animationDuringScreenOffReported = true } } override fun onAnimationCancel(animation: Animator) {} override fun onAnimationRepeat(animation: Animator) {} } /** Used to record the stack trace of animation starter. */ private class AnimationDuringScreenOffException(message: String) : RuntimeException(message)