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)
|
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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user