Merge "Affordances no longer clickable when invisible." into tm-qpr-dev

This commit is contained in:
Alejandro Nijamkin
2022-08-23 14:14:48 +00:00
committed by Android (Google) Code Review
4 changed files with 184 additions and 4 deletions

View File

@@ -251,9 +251,21 @@ object KeyguardBottomAreaViewBinder {
Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface) Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId) 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)) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return@setOnClickListener return
} }
if (viewModel.configKey != null) { if (viewModel.configKey != null) {

View File

@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel package com.android.systemui.keyguard.ui.viewmodel
import androidx.annotation.VisibleForTesting
import com.android.systemui.doze.util.BurnInHelperWrapper import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -37,6 +38,23 @@ constructor(
private val bottomAreaInteractor: KeyguardBottomAreaInteractor, private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
private val burnInHelperWrapper: BurnInHelperWrapper, 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. */ /** An observable for the view-model of the "start button" quick affordance. */
val startButton: Flow<KeyguardQuickAffordanceViewModel> = val startButton: Flow<KeyguardQuickAffordanceViewModel> =
button(KeyguardQuickAffordancePosition.BOTTOM_START) button(KeyguardQuickAffordancePosition.BOTTOM_START)
@@ -77,14 +95,19 @@ constructor(
return combine( return combine(
quickAffordanceInteractor.quickAffordance(position), quickAffordanceInteractor.quickAffordance(position),
bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(), bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
) { model, animateReveal -> areQuickAffordancesFullyOpaque,
model.toViewModel(animateReveal) ) { model, animateReveal, isFullyOpaque ->
model.toViewModel(
animateReveal = animateReveal,
isClickable = isFullyOpaque,
)
} }
.distinctUntilChanged() .distinctUntilChanged()
} }
private fun KeyguardQuickAffordanceModel.toViewModel( private fun KeyguardQuickAffordanceModel.toViewModel(
animateReveal: Boolean, animateReveal: Boolean,
isClickable: Boolean,
): KeyguardQuickAffordanceViewModel { ): KeyguardQuickAffordanceViewModel {
return when (this) { return when (this) {
is KeyguardQuickAffordanceModel.Visible -> is KeyguardQuickAffordanceModel.Visible ->
@@ -100,8 +123,20 @@ constructor(
animationController = parameters.animationController, animationController = parameters.animationController,
) )
}, },
isClickable = isClickable,
) )
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel() 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
}
} }

View File

@@ -31,6 +31,7 @@ data class KeyguardQuickAffordanceViewModel(
val icon: ContainedDrawable = ContainedDrawable.WithResource(0), val icon: ContainedDrawable = ContainedDrawable.WithResource(0),
@StringRes val contentDescriptionResourceId: Int = 0, @StringRes val contentDescriptionResourceId: Int = 0,
val onClicked: (OnClickedParameters) -> Unit = {}, val onClicked: (OnClickedParameters) -> Unit = {},
val isClickable: Boolean = false,
) { ) {
data class OnClickedParameters( data class OnClickedParameters(
val configKey: KClass<out KeyguardQuickAffordanceConfig>, val configKey: KClass<out KeyguardQuickAffordanceConfig>,

View File

@@ -37,6 +37,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlin.math.max
import kotlin.math.min
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -127,6 +129,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
val testConfig = val testConfig =
TestConfig( TestConfig(
isVisible = true, isVisible = true,
isClickable = true,
icon = mock(), icon = mock(),
canShowWhileLocked = false, canShowWhileLocked = false,
intent = Intent("action"), intent = Intent("action"),
@@ -154,6 +157,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
val config = val config =
TestConfig( TestConfig(
isVisible = true, isVisible = true,
isClickable = true,
icon = mock(), icon = mock(),
canShowWhileLocked = false, canShowWhileLocked = false,
intent = null, // This will cause it to tell the system that the click was handled. intent = null, // This will cause it to tell the system that the click was handled.
@@ -201,6 +205,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
val testConfig = val testConfig =
TestConfig( TestConfig(
isVisible = true, isVisible = true,
isClickable = true,
icon = mock(), icon = mock(),
canShowWhileLocked = false, canShowWhileLocked = false,
intent = Intent("action"), intent = Intent("action"),
@@ -260,6 +265,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
testConfig = testConfig =
TestConfig( TestConfig(
isVisible = true, isVisible = true,
isClickable = true,
icon = mock(), icon = mock(),
canShowWhileLocked = true, canShowWhileLocked = true,
) )
@@ -269,6 +275,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
testConfig = testConfig =
TestConfig( TestConfig(
isVisible = true, isVisible = true,
isClickable = true,
icon = mock(), icon = mock(),
canShowWhileLocked = false, canShowWhileLocked = false,
) )
@@ -342,6 +349,129 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
job.cancel() 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 { private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
repository.setDozeAmount(dozeAmount) repository.setDozeAmount(dozeAmount)
return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET) return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
@@ -384,6 +514,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
) { ) {
checkNotNull(viewModel) checkNotNull(viewModel)
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible) assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
if (testConfig.isVisible) { if (testConfig.isVisible) {
assertThat(viewModel.icon).isEqualTo(testConfig.icon) assertThat(viewModel.icon).isEqualTo(testConfig.icon)
viewModel.onClicked.invoke( viewModel.onClicked.invoke(
@@ -404,6 +535,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
private data class TestConfig( private data class TestConfig(
val isVisible: Boolean, val isVisible: Boolean,
val isClickable: Boolean = false,
val icon: ContainedDrawable? = null, val icon: ContainedDrawable? = null,
val canShowWhileLocked: Boolean = false, val canShowWhileLocked: Boolean = false,
val intent: Intent? = null, val intent: Intent? = null,