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

Commit eb173f22 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin Committed by Automerger Merge Worker
Browse files

Merge "Affordances no longer clickable when invisible." into tm-qpr-dev am: 8918b53f

parents 162dcbf9 8918b53f
Loading
Loading
Loading
Loading
+14 −2
Original line number Diff line number Diff line
@@ -251,9 +251,21 @@ object KeyguardBottomAreaViewBinder {
            Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)

        view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId)
        view.setOnClickListener {
        view.isClickable = viewModel.isClickable
        if (viewModel.isClickable) {
            view.setOnClickListener(OnClickListener(viewModel, falsingManager))
        } else {
            view.setOnClickListener(null)
        }
    }

    private class OnClickListener(
        private val viewModel: KeyguardQuickAffordanceViewModel,
        private val falsingManager: FalsingManager,
    ) : View.OnClickListener {
        override fun onClick(view: View) {
            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
                return@setOnClickListener
                return
            }

            if (viewModel.configKey != null) {
+37 −2
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.keyguard.ui.viewmodel

import androidx.annotation.VisibleForTesting
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -37,6 +38,23 @@ constructor(
    private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
    private val burnInHelperWrapper: BurnInHelperWrapper,
) {
    /**
     * Whether quick affordances are "opaque enough" to be considered visible to and interactive by
     * the user. If they are not interactive, user input should not be allowed on them.
     *
     * Note that there is a margin of error, where we allow very, very slightly transparent views to
     * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the
     * error margin of floating point arithmetic.
     *
     * A view that is visible but with an alpha of less than our threshold either means it's not
     * fully done fading in or is fading/faded out. Either way, it should not be
     * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987.
     */
    private val areQuickAffordancesFullyOpaque: Flow<Boolean> =
        bottomAreaInteractor.alpha
            .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD }
            .distinctUntilChanged()

    /** An observable for the view-model of the "start button" quick affordance. */
    val startButton: Flow<KeyguardQuickAffordanceViewModel> =
        button(KeyguardQuickAffordancePosition.BOTTOM_START)
@@ -77,14 +95,19 @@ constructor(
        return combine(
                quickAffordanceInteractor.quickAffordance(position),
                bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
            ) { model, animateReveal ->
                model.toViewModel(animateReveal)
                areQuickAffordancesFullyOpaque,
            ) { model, animateReveal, isFullyOpaque ->
                model.toViewModel(
                    animateReveal = animateReveal,
                    isClickable = isFullyOpaque,
                )
            }
            .distinctUntilChanged()
    }

    private fun KeyguardQuickAffordanceModel.toViewModel(
        animateReveal: Boolean,
        isClickable: Boolean,
    ): KeyguardQuickAffordanceViewModel {
        return when (this) {
            is KeyguardQuickAffordanceModel.Visible ->
@@ -100,8 +123,20 @@ constructor(
                            animationController = parameters.animationController,
                        )
                    },
                    isClickable = isClickable,
                )
            is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
        }
    }

    companion object {
        // We select a value that's less than 1.0 because we want floating point math precision to
        // not be a factor in determining whether the affordance UI is fully opaque. The number we
        // choose needs to be close enough 1.0 such that the user can't easily tell the difference
        // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same
        // time, we don't want the number to be too close to 1.0 such that there is a chance that we
        // never treat the affordance UI as "fully opaque" as that would risk making it forever not
        // clickable.
        @VisibleForTesting const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ data class KeyguardQuickAffordanceViewModel(
    val icon: ContainedDrawable = ContainedDrawable.WithResource(0),
    @StringRes val contentDescriptionResourceId: Int = 0,
    val onClicked: (OnClickedParameters) -> Unit = {},
    val isClickable: Boolean = false,
) {
    data class OnClickedParameters(
        val configKey: KClass<out KeyguardQuickAffordanceConfig>,
+132 −0
Original line number Diff line number Diff line
@@ -37,6 +37,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlin.math.max
import kotlin.math.min
import kotlin.reflect.KClass
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -127,6 +129,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
        val testConfig =
            TestConfig(
                isVisible = true,
                isClickable = true,
                icon = mock(),
                canShowWhileLocked = false,
                intent = Intent("action"),
@@ -154,6 +157,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
        val config =
            TestConfig(
                isVisible = true,
                isClickable = true,
                icon = mock(),
                canShowWhileLocked = false,
                intent = null, // This will cause it to tell the system that the click was handled.
@@ -201,6 +205,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
        val testConfig =
            TestConfig(
                isVisible = true,
                isClickable = true,
                icon = mock(),
                canShowWhileLocked = false,
                intent = Intent("action"),
@@ -260,6 +265,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
            testConfig =
                TestConfig(
                    isVisible = true,
                    isClickable = true,
                    icon = mock(),
                    canShowWhileLocked = true,
                )
@@ -269,6 +275,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
            testConfig =
                TestConfig(
                    isVisible = true,
                    isClickable = true,
                    icon = mock(),
                    canShowWhileLocked = false,
                )
@@ -342,6 +349,129 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
        job.cancel()
    }

    @Test
    fun `isClickable - true when alpha at threshold`() = runBlockingTest {
        repository.setKeyguardShowing(true)
        repository.setBottomAreaAlpha(
            KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
        )

        val testConfig =
            TestConfig(
                isVisible = true,
                isClickable = true,
                icon = mock(),
                canShowWhileLocked = false,
                intent = Intent("action"),
            )
        val configKey =
            setUpQuickAffordanceModel(
                position = KeyguardQuickAffordancePosition.BOTTOM_START,
                testConfig = testConfig,
            )

        var latest: KeyguardQuickAffordanceViewModel? = null
        val job = underTest.startButton.onEach { latest = it }.launchIn(this)

        assertQuickAffordanceViewModel(
            viewModel = latest,
            testConfig = testConfig,
            configKey = configKey,
        )
        job.cancel()
    }

    @Test
    fun `isClickable - true when alpha above threshold`() = runBlockingTest {
        repository.setKeyguardShowing(true)
        var latest: KeyguardQuickAffordanceViewModel? = null
        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
        repository.setBottomAreaAlpha(
            min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
        )

        val testConfig =
            TestConfig(
                isVisible = true,
                isClickable = true,
                icon = mock(),
                canShowWhileLocked = false,
                intent = Intent("action"),
            )
        val configKey =
            setUpQuickAffordanceModel(
                position = KeyguardQuickAffordancePosition.BOTTOM_START,
                testConfig = testConfig,
            )

        assertQuickAffordanceViewModel(
            viewModel = latest,
            testConfig = testConfig,
            configKey = configKey,
        )
        job.cancel()
    }

    @Test
    fun `isClickable - false when alpha below threshold`() = runBlockingTest {
        repository.setKeyguardShowing(true)
        var latest: KeyguardQuickAffordanceViewModel? = null
        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
        repository.setBottomAreaAlpha(
            max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
        )

        val testConfig =
            TestConfig(
                isVisible = true,
                isClickable = false,
                icon = mock(),
                canShowWhileLocked = false,
                intent = Intent("action"),
            )
        val configKey =
            setUpQuickAffordanceModel(
                position = KeyguardQuickAffordancePosition.BOTTOM_START,
                testConfig = testConfig,
            )

        assertQuickAffordanceViewModel(
            viewModel = latest,
            testConfig = testConfig,
            configKey = configKey,
        )
        job.cancel()
    }

    @Test
    fun `isClickable - false when alpha at zero`() = runBlockingTest {
        repository.setKeyguardShowing(true)
        var latest: KeyguardQuickAffordanceViewModel? = null
        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
        repository.setBottomAreaAlpha(0f)

        val testConfig =
            TestConfig(
                isVisible = true,
                isClickable = false,
                icon = mock(),
                canShowWhileLocked = false,
                intent = Intent("action"),
            )
        val configKey =
            setUpQuickAffordanceModel(
                position = KeyguardQuickAffordancePosition.BOTTOM_START,
                testConfig = testConfig,
            )

        assertQuickAffordanceViewModel(
            viewModel = latest,
            testConfig = testConfig,
            configKey = configKey,
        )
        job.cancel()
    }

    private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
        repository.setDozeAmount(dozeAmount)
        return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
@@ -384,6 +514,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
    ) {
        checkNotNull(viewModel)
        assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
        assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
        if (testConfig.isVisible) {
            assertThat(viewModel.icon).isEqualTo(testConfig.icon)
            viewModel.onClicked.invoke(
@@ -404,6 +535,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {

    private data class TestConfig(
        val isVisible: Boolean,
        val isClickable: Boolean = false,
        val icon: ContainedDrawable? = null,
        val canShowWhileLocked: Boolean = false,
        val intent: Intent? = null,