Merge "Affordances no longer clickable when invisible." into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
8918b53fe3
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user