blob: 8758ce5eade626049c1c7ba34f6b157f10a789e3 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.keyguard.ui.viewmodel
import android.content.Intent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.containeddrawable.ContainedDrawable
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.usecase.FakeLaunchKeyguardQuickAffordanceUseCase
import com.android.systemui.keyguard.domain.usecase.FakeObserveKeyguardQuickAffordanceUseCase
import com.android.systemui.keyguard.domain.usecase.ObserveAnimateBottomAreaTransitionsUseCase
import com.android.systemui.keyguard.domain.usecase.ObserveBottomAreaAlphaUseCase
import com.android.systemui.keyguard.domain.usecase.ObserveClockPositionUseCase
import com.android.systemui.keyguard.domain.usecase.ObserveDozeAmountUseCase
import com.android.systemui.keyguard.domain.usecase.ObserveIsDozingUseCase
import com.android.systemui.keyguard.domain.usecase.OnKeyguardQuickAffordanceClickedUseCase
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlin.reflect.KClass
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(JUnit4::class)
class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
@Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
@Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
private lateinit var underTest: KeyguardBottomAreaViewModel
private lateinit var repository: FakeKeyguardRepository
private lateinit var registry: FakeKeyguardQuickAffordanceRegistry
private lateinit var isDozingUseCase: ObserveIsDozingUseCase
private lateinit var launchQuickAffordanceUseCase: FakeLaunchKeyguardQuickAffordanceUseCase
private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig
private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig
private lateinit var qrCodeScannerAffordanceConfig: FakeKeyguardQuickAffordanceConfig
private lateinit var observeQuickAffordanceUseCase: FakeObserveKeyguardQuickAffordanceUseCase
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
.thenReturn(RETURNED_BURN_IN_OFFSET)
homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
registry =
FakeKeyguardQuickAffordanceRegistry(
mapOf(
KeyguardQuickAffordancePosition.BOTTOM_START to
listOf(
homeControlsQuickAffordanceConfig,
),
KeyguardQuickAffordancePosition.BOTTOM_END to
listOf(
quickAccessWalletAffordanceConfig,
qrCodeScannerAffordanceConfig,
),
),
)
repository = FakeKeyguardRepository()
isDozingUseCase =
ObserveIsDozingUseCase(
repository = repository,
)
launchQuickAffordanceUseCase = FakeLaunchKeyguardQuickAffordanceUseCase()
observeQuickAffordanceUseCase = FakeObserveKeyguardQuickAffordanceUseCase()
underTest =
KeyguardBottomAreaViewModel(
observeQuickAffordanceUseCase = observeQuickAffordanceUseCase,
onQuickAffordanceClickedUseCase =
OnKeyguardQuickAffordanceClickedUseCase(
registry =
FakeKeyguardQuickAffordanceRegistry(
mapOf(
KeyguardQuickAffordancePosition.BOTTOM_START to
listOf(
homeControlsQuickAffordanceConfig,
),
KeyguardQuickAffordancePosition.BOTTOM_END to
listOf(
quickAccessWalletAffordanceConfig,
qrCodeScannerAffordanceConfig,
),
),
),
launchAffordanceUseCase = launchQuickAffordanceUseCase,
),
observeBottomAreaAlphaUseCase =
ObserveBottomAreaAlphaUseCase(
repository = repository,
),
observeIsDozingUseCase = isDozingUseCase,
observeAnimateBottomAreaTransitionsUseCase =
ObserveAnimateBottomAreaTransitionsUseCase(
repository = repository,
),
observeDozeAmountUseCase =
ObserveDozeAmountUseCase(
repository = repository,
),
observeClockPositionUseCase =
ObserveClockPositionUseCase(
repository = repository,
),
burnInHelperWrapper = burnInHelperWrapper,
)
}
@Test
fun `startButton - present - visible model - starts activity on click`() = runBlockingTest {
var latest: KeyguardQuickAffordanceViewModel? = null
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
val testConfig =
TestConfig(
isVisible = 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 `endButton - present - visible model - do nothing on click`() = runBlockingTest {
var latest: KeyguardQuickAffordanceViewModel? = null
val job = underTest.endButton.onEach { latest = it }.launchIn(this)
val config =
TestConfig(
isVisible = true,
icon = mock(),
canShowWhileLocked = false,
intent = null, // This will cause it to tell the system that the click was handled.
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_END,
testConfig = config,
)
assertQuickAffordanceViewModel(
viewModel = latest,
testConfig = config,
configKey = configKey,
)
job.cancel()
}
@Test
fun `startButton - not present - model is hidden`() = runBlockingTest {
var latest: KeyguardQuickAffordanceViewModel? = null
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
val config =
TestConfig(
isVisible = false,
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = config,
)
assertQuickAffordanceViewModel(
viewModel = latest,
testConfig = config,
configKey = configKey,
)
job.cancel()
}
@Test
fun animateButtonReveal() = runBlockingTest {
val values = mutableListOf<Boolean>()
val job = underTest.animateButtonReveal.onEach(values::add).launchIn(this)
repository.setAnimateDozingTransitions(true)
repository.setAnimateDozingTransitions(false)
assertThat(values).isEqualTo(listOf(false, true, false))
job.cancel()
}
@Test
fun isOverlayContainerVisible() = runBlockingTest {
val values = mutableListOf<Boolean>()
val job = underTest.isOverlayContainerVisible.onEach(values::add).launchIn(this)
repository.setDozing(true)
repository.setDozing(false)
assertThat(values).isEqualTo(listOf(true, false, true))
job.cancel()
}
@Test
fun alpha() = runBlockingTest {
val values = mutableListOf<Float>()
val job = underTest.alpha.onEach(values::add).launchIn(this)
repository.setBottomAreaAlpha(0.1f)
repository.setBottomAreaAlpha(0.5f)
repository.setBottomAreaAlpha(0.2f)
repository.setBottomAreaAlpha(0f)
assertThat(values).isEqualTo(listOf(1f, 0.1f, 0.5f, 0.2f, 0f))
job.cancel()
}
@Test
fun isIndicationAreaPadded() = runBlockingTest {
repository.setKeyguardShowing(true)
val values = mutableListOf<Boolean>()
val job = underTest.isIndicationAreaPadded.onEach(values::add).launchIn(this)
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig =
TestConfig(
isVisible = true,
icon = mock(),
canShowWhileLocked = true,
)
)
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_END,
testConfig =
TestConfig(
isVisible = true,
icon = mock(),
canShowWhileLocked = false,
)
)
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig =
TestConfig(
isVisible = false,
)
)
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_END,
testConfig =
TestConfig(
isVisible = false,
)
)
assertThat(values)
.isEqualTo(
listOf(
// Initially, no button is visible so the indication area is not padded.
false,
// Once we add the first visible button, the indication area becomes padded.
// This
// continues to be true after we add the second visible button and even after we
// make the first button not visible anymore.
true,
// Once both buttons are not visible, the indication area is, again, not padded.
false,
)
)
job.cancel()
}
@Test
fun indicationAreaTranslationX() = runBlockingTest {
val values = mutableListOf<Float>()
val job = underTest.indicationAreaTranslationX.onEach(values::add).launchIn(this)
repository.setClockPosition(100, 100)
repository.setClockPosition(200, 100)
repository.setClockPosition(200, 200)
repository.setClockPosition(300, 100)
assertThat(values).isEqualTo(listOf(0f, 100f, 200f, 300f))
job.cancel()
}
@Test
fun indicationAreaTranslationY() = runBlockingTest {
val values = mutableListOf<Float>()
val job =
underTest
.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET)
.onEach(values::add)
.launchIn(this)
val expectedTranslationValues =
listOf(
-0f, // Negative 0 - apparently there's a difference in floating point arithmetic -
// FML
setDozeAmountAndCalculateExpectedTranslationY(0.1f),
setDozeAmountAndCalculateExpectedTranslationY(0.2f),
setDozeAmountAndCalculateExpectedTranslationY(0.5f),
setDozeAmountAndCalculateExpectedTranslationY(1f),
)
assertThat(values).isEqualTo(expectedTranslationValues)
job.cancel()
}
private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
repository.setDozeAmount(dozeAmount)
return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
}
private suspend fun setUpQuickAffordanceModel(
position: KeyguardQuickAffordancePosition,
testConfig: TestConfig,
): KClass<*> {
val config =
when (position) {
KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig
KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig
}
val state =
if (testConfig.isVisible) {
if (testConfig.intent != null) {
config.onClickedResult =
KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
intent = testConfig.intent,
canShowWhileLocked = testConfig.canShowWhileLocked,
)
}
KeyguardQuickAffordanceConfig.State.Visible(
icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
)
} else {
KeyguardQuickAffordanceConfig.State.Hidden
}
config.setState(state)
val configKey = config::class
observeQuickAffordanceUseCase.setModel(
position,
KeyguardQuickAffordanceModel.from(state, configKey)
)
return configKey
}
private fun assertQuickAffordanceViewModel(
viewModel: KeyguardQuickAffordanceViewModel?,
testConfig: TestConfig,
configKey: KClass<*>,
) {
checkNotNull(viewModel)
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
if (testConfig.isVisible) {
assertThat(viewModel.icon).isEqualTo(testConfig.icon)
viewModel.onClicked.invoke(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = configKey,
animationController = animationController,
)
)
testConfig.intent?.let { intent ->
assertThat(launchQuickAffordanceUseCase.invocations)
.isEqualTo(
listOf(
FakeLaunchKeyguardQuickAffordanceUseCase.Invocation(
intent = intent,
canShowWhileLocked = testConfig.canShowWhileLocked,
animationController = animationController,
)
)
)
}
?: run { assertThat(launchQuickAffordanceUseCase.invocations).isEmpty() }
} else {
assertThat(viewModel.isVisible).isFalse()
}
}
private data class TestConfig(
val isVisible: Boolean,
val icon: ContainedDrawable? = null,
val canShowWhileLocked: Boolean = false,
val intent: Intent? = null,
) {
init {
check(!isVisible || icon != null) { "Must supply non-null icon if visible!" }
}
}
companion object {
private const val DEFAULT_BURN_IN_OFFSET = 5
private const val RETURNED_BURN_IN_OFFSET = 3
private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
}
}