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

Commit e722242a authored by Chandru S's avatar Chandru S
Browse files

Possible performance fix for missed frames and jank with bouncer blur

Keyguard transitions produce frame aligned values that are backed by an animator. Collecting them directly in the view binder will minimize any possible jank and frame deadline misses.

Bug: 393199337
Bug: 393219138
Flag: com.android.systemui.bouncer_ui_revamp
Test: verified bouncer and glanceable hub CUJs manually
Test: updated unit tests
Change-Id: I0f5c4c31fa660adf3795ad9e1de6e1ced575106c
parent f2f7607a
Loading
Loading
Loading
Loading
+0 −13
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -36,18 +35,6 @@ class WindowRootViewBlurInteractorTest : SysuiTestCase() {

    val underTest by lazy { kosmos.windowRootViewBlurInteractor }

    @Test
    fun bouncerBlurIsAppliedImmediately() =
        testScope.runTest {
            val blurRadius by collectLastValue(underTest.blurRadius)
            val isBlurOpaque by collectLastValue(underTest.isBlurOpaque)

            underTest.requestBlurForBouncer(10)

            assertThat(blurRadius).isEqualTo(10)
            assertThat(isBlurOpaque).isFalse()
        }

    @Test
    fun shadeBlurIsNotAppliedWhenBouncerBlurIsActive() =
        testScope.runTest {
+4 −2
Original line number Diff line number Diff line
@@ -46,12 +46,14 @@ class WindowRootViewModelTest : SysuiTestCase() {
    @Test
    fun bouncerTransitionChangesWindowBlurRadius() =
        testScope.runTest {
            val blurState by collectLastValue(underTest.blurState)
            val blurRadius by collectLastValue(underTest.blurRadius)
            val isBlurOpaque by collectLastValue(underTest.isBlurOpaque)
            runCurrent()

            kosmos.fakeBouncerTransitions.first().windowBlurRadius.value = 30.0f
            runCurrent()

            assertThat(blurState).isEqualTo(BlurState(radius = 30, isOpaque = false))
            assertThat(blurRadius).isEqualTo(30)
            assertThat(isBlurOpaque).isEqualTo(false)
        }
}
+28 −28
Original line number Diff line number Diff line
@@ -32,7 +32,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@@ -72,37 +74,27 @@ constructor(
    /** Radius of blur to be applied on the window root view. */
    val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow()

    /** Whether the blur applied is opaque or transparent. */
    val isBlurOpaque: StateFlow<Boolean> = repository.isBlurOpaque.asStateFlow()

    /**
     * Emits the applied blur radius whenever blur is successfully applied to the window root view.
     */
    val onBlurAppliedEvent: Flow<Int> = repository.onBlurApplied

    /**
     * Request to apply blur while on bouncer, this takes precedence over other blurs (from shade).
     */
    fun requestBlurForBouncer(blurRadius: Int) {
        repository.isBlurOpaque.value = false
        repository.blurRadius.value = blurRadius
    }

    /**
     * Request to apply blur while on glanceable hub, this takes precedence over other blurs (from
     * shade) except for bouncer.
     */
    fun requestBlurForGlanceableHub(blurRadius: Int): Boolean {
        if (keyguardInteractor.primaryBouncerShowing.value) {
            return false
        }

        Log.d(TAG, "requestBlurForGlanceableHub for $blurRadius")

        repository.isBlurOpaque.value = false
        repository.blurRadius.value = blurRadius

        return true
    /** Whether the blur applied is opaque or transparent. */
    val isBlurOpaque: Flow<Boolean> =
        combine(
            if (Flags.bouncerUiRevamp()) {
                keyguardInteractor.primaryBouncerShowing.or(isBouncerTransitionInProgress)
            } else {
                flowOf(false)
            },
            if (Flags.glanceableHubBlurredBackground()) {
                communalInteractor.isCommunalBlurring
            } else {
                flowOf(false)
            },
            repository.isBlurOpaque,
        ) { bouncerActive, ghActive, shadeBlurOpaque ->
            if (bouncerActive || ghActive) false else shadeBlurOpaque
        }

    /**
@@ -119,10 +111,10 @@ constructor(
        // We need to check either of these because they are two different sources of truth,
        // primaryBouncerShowing changes early to true/false, but blur is
        // coordinated by transition value.
        if (keyguardInteractor.primaryBouncerShowing.value || isBouncerTransitionInProgress.value) {
        if (isBouncerTransitionInProgress()) {
            return false
        }
        if (communalInteractor.isCommunalBlurring.value) {
        if (isGlanceableHubActive()) {
            return false
        }
        Log.d(TAG, "requestingBlurForShade for $blurRadius $opaque")
@@ -131,6 +123,14 @@ constructor(
        return true
    }

    private fun isGlanceableHubActive() = communalInteractor.isCommunalBlurring.value

    private fun isBouncerTransitionInProgress() =
        keyguardInteractor.primaryBouncerShowing.value || isBouncerTransitionInProgress.value

    private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>): Flow<Boolean> =
        this.combine(anotherFlow) { a, b -> a || b }

    companion object {
        const val TAG = "WindowRootViewBlurInteractor"
    }
+39 −25
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.util.Log
import android.view.Choreographer
import android.view.Choreographer.FrameCallback
import com.android.app.tracing.coroutines.TrackTracer
import com.android.app.tracing.coroutines.launchTraced
import com.android.systemui.Flags
import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -29,8 +30,8 @@ import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.window.ui.viewmodel.WindowRootViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch

/**
 * View binder that wires up window level UI transformations like blur to the [WindowRootView]
@@ -51,7 +52,6 @@ object WindowRootViewBinder {

        view.repeatWhenAttached(mainDispatcher) {
            Log.d(TAG, "Binding root view")
            var frameCallbackPendingExecution: FrameCallback? = null
            view.viewModel(
                minWindowLifecycleState = WindowLifecycleState.ATTACHED,
                factory = { viewModelFactory.create() },
@@ -59,34 +59,48 @@ object WindowRootViewBinder {
            ) { viewModel ->
                try {
                    Log.d(TAG, "Launching coroutines that update window root view state")
                    launch {
                        viewModel.blurState
                            .filter { it.radius >= 0 }
                            .collect { blurState ->
                    launchTraced("WindowBlur") {
                        var wasUpdateScheduledForThisFrame = false
                        var lastScheduledBlurRadius = 0
                        var lastScheduleBlurOpaqueness = false

                        // Creating the callback once and not for every coroutine invocation
                        val newFrameCallback = FrameCallback {
                                    frameCallbackPendingExecution = null
                            wasUpdateScheduledForThisFrame = false
                            val blurRadiusToApply = lastScheduledBlurRadius
                            blurUtils.applyBlur(
                                view.rootView?.viewRootImpl,
                                        blurState.radius,
                                        blurState.isOpaque,
                                blurRadiusToApply,
                                lastScheduleBlurOpaqueness,
                            )
                            TrackTracer.instantForGroup(
                                "windowBlur",
                                "appliedBlurRadius",
                                        blurState.radius,
                                blurRadiusToApply,
                            )
                                    viewModel.onBlurApplied(blurState.radius)
                            viewModel.onBlurApplied(blurRadiusToApply)
                        }

                        combine(viewModel.blurRadius, viewModel.isBlurOpaque, ::Pair)
                            .filter { it.first >= 0 }
                            .collect { (blurRadius, isOpaque) ->
                                // Expectation is that we schedule only one blur radius value
                                // per frame
                                if (wasUpdateScheduledForThisFrame) {
                                    return@collect
                                }
                                TrackTracer.instantForGroup(
                                    "windowBlur",
                                    "preparedBlurRadius",
                                    blurState.radius,
                                    blurRadius,
                                )
                                lastScheduledBlurRadius = blurRadius.toInt()
                                lastScheduleBlurOpaqueness = isOpaque
                                wasUpdateScheduledForThisFrame = true
                                blurUtils.prepareBlur(
                                    view.rootView?.viewRootImpl,
                                    lastScheduledBlurRadius,
                                )
                                blurUtils.prepareBlur(view.rootView?.viewRootImpl, blurState.radius)
                                if (frameCallbackPendingExecution != null) {
                                    choreographer.removeFrameCallback(frameCallbackPendingExecution)
                                }
                                frameCallbackPendingExecution = newFrameCallback
                                choreographer.postFrameCallback(newFrameCallback)
                            }
                    }
+32 −52
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ package com.android.systemui.window.ui.viewmodel
import android.os.Build
import android.util.Log
import com.android.app.tracing.coroutines.launchTraced
import com.android.systemui.Flags.glanceableHubBlurredBackground
import com.android.systemui.Flags
import com.android.systemui.keyguard.ui.transitions.GlanceableHubTransition
import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -29,9 +29,9 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach

@@ -41,14 +41,33 @@ typealias BlurAppliedUiEvent = Int
class WindowRootViewModel
@AssistedInject
constructor(
    private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>,
    private val glanceableHubTransitions: Set<@JvmSuppressWildcards GlanceableHubTransition>,
    primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>,
    glanceableHubTransitions: Set<@JvmSuppressWildcards GlanceableHubTransition>,
    private val blurInteractor: WindowRootViewBlurInteractor,
) : ExclusiveActivatable() {

    private val blurEvents = Channel<BlurAppliedUiEvent>(Channel.BUFFERED)
    private val _blurState = MutableStateFlow(BlurState(0, false))
    val blurState = _blurState.asStateFlow()

    private val bouncerBlurRadiusFlows =
        if (Flags.bouncerUiRevamp())
            primaryBouncerTransitions.map { it.windowBlurRadius.logIfPossible(it.javaClass.name) }
        else emptyList()

    private val glanceableHubBlurRadiusFlows =
        if (Flags.glanceableHubBlurredBackground())
            glanceableHubTransitions.map { it.windowBlurRadius.logIfPossible(it.javaClass.name) }
        else emptyList()

    val blurRadius: Flow<Float> =
        listOf(
                *bouncerBlurRadiusFlows.toTypedArray(),
                *glanceableHubBlurRadiusFlows.toTypedArray(),
                blurInteractor.blurRadius.map { it.toFloat() }.logIfPossible("ShadeBlur"),
            )
            .merge()

    val isBlurOpaque =
        blurInteractor.isBlurOpaque.distinctUntilChanged().logIfPossible("isBlurOpaque")

    override suspend fun onActivated(): Nothing {
        coroutineScope {
@@ -60,49 +79,6 @@ constructor(
                    blurInteractor.onBlurApplied(event)
                }
            }

            launchTraced("WindowRootViewModel#blurState") {
                combine(blurInteractor.blurRadius, blurInteractor.isBlurOpaque, ::BlurState)
                    .collect { _blurState.value = it }
            }

            launchTraced("WindowRootViewModel#bouncerTransitions") {
                primaryBouncerTransitions
                    .map { transition ->
                        transition.windowBlurRadius.onEach { blurRadius ->
                            if (isLoggable) {
                                Log.d(
                                    TAG,
                                    "${transition.javaClass.simpleName} windowBlurRadius $blurRadius",
                                )
                            }
                        }
                    }
                    .merge()
                    .collect { blurRadius ->
                        blurInteractor.requestBlurForBouncer(blurRadius.toInt())
                    }
            }

            if (glanceableHubBlurredBackground()) {
                launchTraced("WindowRootViewModel#glanceableHubTransitions") {
                    glanceableHubTransitions
                        .map { transition ->
                            transition.windowBlurRadius.onEach { blurRadius ->
                                if (isLoggable) {
                                    Log.d(
                                        TAG,
                                        "${transition.javaClass.simpleName} windowBlurRadius $blurRadius",
                                    )
                                }
                            }
                        }
                        .merge()
                        .collect { blurRadius ->
                            blurInteractor.requestBlurForGlanceableHub(blurRadius.toInt())
                        }
                }
            }
        }
        awaitCancellation()
    }
@@ -118,7 +94,11 @@ constructor(

    private companion object {
        const val TAG = "WindowRootViewModel"
        val isLoggable = Log.isLoggable(TAG, Log.DEBUG) || Build.isDebuggable()
        val isLoggable = Log.isLoggable(TAG, Log.VERBOSE) || Build.isDebuggable()

        fun <T> Flow<T>.logIfPossible(loggingInfo: String): Flow<T> {
            return onEach { if (isLoggable) Log.v(TAG, "$loggingInfo $it") }
        }
    }
}