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

Commit c6366208 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Create screen off animation guard for generic ValueAnimator" into main

parents 28b3a25b eb03b2e4
Loading
Loading
Loading
Loading
+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)
    }
}
+64 −2
Original line number Diff line number Diff line
@@ -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
@@ -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
    }

@@ -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)