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

Commit 67d9264f authored by Chandru S's avatar Chandru S Committed by Android (Google) Code Review
Browse files

Merge "Possible performance fix for missed frames and jank with bouncer blur" into main

parents 0496b1f5 e722242a
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") }
        }
    }
}