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

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19680529

Change-Id: I88fc2afd120d339020dbc106dce8177b022df0db
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Alejandro Nijamkin
2022-08-23 20:34:40 +00:00
committed by Automerger Merge Worker
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)
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) {

View File

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

View File

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

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.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,