/* * Copyright (C) 2023 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.bouncer.ui.viewmodel import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.authenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point import com.android.systemui.haptics.msdl.bouncerHapticPlayer import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.testKosmos import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) class PatternBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val onIntentionalUserInputMock: () -> Unit = mock() private val underTest = kosmos.patternBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true).asStateFlow(), onIntentionalUserInput = onIntentionalUserInputMock, bouncerHapticPlayer = kosmos.bouncerHapticPlayer, ) private val containerSize = 90 // px private val dotSize = 30 // px @Before fun setUp() { overrideResource(R.string.keyguard_enter_your_pattern, ENTER_YOUR_PATTERN) overrideResource(R.string.kg_wrong_pattern, WRONG_PATTERN) underTest.activateIn(kosmos.testScope) } @Test fun onShown() = kosmos.runTest { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentOverlays).contains(Overlays.Bouncer) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pattern) } @Test fun onDragStart() = kosmos.runTest { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() verify(onIntentionalUserInputMock, never()).invoke() underTest.onDragStart() verify(onIntentionalUserInputMock, times(1)).invoke() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentOverlays).contains(Overlays.Bouncer) } @Test fun onDragEnd_whenCorrect() = kosmos.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() underTest.onDragStart() assertThat(currentDot).isNull() CORRECT_PATTERN.forEachIndexed { index, coordinate -> dragToCoordinate(coordinate) assertWithMessage("Wrong selected dots for index $index") .that(selectedDots) .isEqualTo( CORRECT_PATTERN.subList(0, index + 1).map { PatternDotViewModel(x = it.x, y = it.y) } ) assertWithMessage("Wrong current dot for index $index") .that(currentDot) .isEqualTo( PatternDotViewModel( x = CORRECT_PATTERN.subList(0, index + 1).last().x, y = CORRECT_PATTERN.subList(0, index + 1).last().y, ) ) } underTest.onDragEnd() assertThat(authResult).isTrue() } @Test fun onDragEnd_whenWrong() = kosmos.runTest { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() underTest.onDragStart() CORRECT_PATTERN.subList(0, 3).forEach { coordinate -> dragToCoordinate(coordinate) } underTest.onDragEnd() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentOverlays).contains(Overlays.Bouncer) } @Test fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheSameRow() = kosmos.runTest { val selectedDots by collectLastValue(underTest.selectedDots) lockDeviceAndOpenPatternBouncer() /* * Pattern setup, coordinates are (column, row) * 0 1 2 * 0 x x x * 1 x x x * 2 x x x */ // Select (0,0), Skip over (1,0) and select (2,0) dragOverCoordinates(Point(0, 0), Point(2, 0)) assertThat(selectedDots) .isEqualTo( listOf( PatternDotViewModel(0, 0), PatternDotViewModel(1, 0), PatternDotViewModel(2, 0), ) ) } @Test fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheSameColumn() = kosmos.runTest { val selectedDots by collectLastValue(underTest.selectedDots) lockDeviceAndOpenPatternBouncer() /* * Pattern setup, coordinates are (column, row) * 0 1 2 * 0 x x x * 1 x x x * 2 x x x */ // Select (1,0), Skip over (1,1) and select (1, 2) dragOverCoordinates(Point(1, 0), Point(1, 2)) assertThat(selectedDots) .isEqualTo( listOf( PatternDotViewModel(1, 0), PatternDotViewModel(1, 1), PatternDotViewModel(1, 2), ) ) } @Test fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheDiagonal() = kosmos.runTest { val selectedDots by collectLastValue(underTest.selectedDots) lockDeviceAndOpenPatternBouncer() /* * Pattern setup * 0 1 2 * 0 x x x * 1 x x x * 2 x x x * * Coordinates are (column, row) * Select (2,0), Skip over (1,1) and select (0, 2) */ dragOverCoordinates(Point(2, 0), Point(0, 2)) assertThat(selectedDots) .isEqualTo( listOf( PatternDotViewModel(2, 0), PatternDotViewModel(1, 1), PatternDotViewModel(0, 2), ) ) } @Test fun onDrag_shouldNotIncludeDotIfItIsNotOnTheLine() = kosmos.runTest { val selectedDots by collectLastValue(underTest.selectedDots) lockDeviceAndOpenPatternBouncer() /* * Pattern setup * 0 1 2 * 0 x x x * 1 x x x * 2 x x x * * Coordinates are (column, row) */ dragOverCoordinates(Point(0, 0), Point(1, 0), Point(2, 0), Point(0, 1)) assertThat(selectedDots) .isEqualTo( listOf( PatternDotViewModel(0, 0), PatternDotViewModel(1, 0), PatternDotViewModel(2, 0), PatternDotViewModel(0, 1), ) ) } @Test fun onDrag_shouldNotIncludeSkippedOverDotsIfTheyAreAlreadySelected() = kosmos.runTest { val selectedDots by collectLastValue(underTest.selectedDots) lockDeviceAndOpenPatternBouncer() /* * Pattern setup * 0 1 2 * 0 x x x * 1 x x x * 2 x x x * * Coordinates are (column, row) */ dragOverCoordinates(Point(1, 0), Point(1, 1), Point(0, 0), Point(2, 0)) assertThat(selectedDots) .isEqualTo( listOf( PatternDotViewModel(1, 0), PatternDotViewModel(1, 1), PatternDotViewModel(0, 0), PatternDotViewModel(2, 0), ) ) } @Test fun onDragEnd_whenPatternTooShort() = kosmos.runTest { val dialogViewModel by collectLastValue(bouncerOverlayContentViewModel.dialogViewModel) lockDeviceAndOpenPatternBouncer() // Enter a pattern that's too short more than enough times that would normally trigger // lockout if the pattern were not too short and wrong: val attempts = FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT + 1 repeat(attempts) { attempt -> underTest.onDragStart() CORRECT_PATTERN.subList(0, authenticationRepository.minPatternLength - 1).forEach { coordinate -> underTest.onDrag( xPx = 30f * coordinate.x + 15, yPx = 30f * coordinate.y + 15, containerSizePx = 90, ) } underTest.onDragEnd() assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull() } } @Test fun onDragEnd_correctAfterWrong() = kosmos.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() underTest.onDragStart() CORRECT_PATTERN.subList(2, 7).forEach(::dragToCoordinate) underTest.onDragEnd() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(authResult).isFalse() // Enter the correct pattern: CORRECT_PATTERN.forEach(::dragToCoordinate) underTest.onDragEnd() assertThat(authResult).isTrue() } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) fun performDotFeedback_deliversDragToken() = kosmos.runTest { underTest.performDotFeedback(null) assertThat(fakeMSDLPlayer.latestTokenPlayed) .isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE) assertThat(fakeMSDLPlayer.latestPropertiesPlayed).isNull() } private fun dragOverCoordinates(vararg coordinatesDragged: Point) { underTest.onDragStart() coordinatesDragged.forEach(::dragToCoordinate) } private fun dragToCoordinate(coordinate: Point) { underTest.onDrag( xPx = dotSize * coordinate.x + 15f, yPx = dotSize * coordinate.y + 15f, containerSizePx = containerSize, ) } private fun Kosmos.lockDeviceAndOpenPatternBouncer() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern) sceneInteractor.showOverlay(Overlays.Bouncer, "reason") runCurrent() assertThat(currentOverlays).contains(Overlays.Bouncer) } companion object { private const val ENTER_YOUR_PATTERN = "Enter your pattern" private const val WRONG_PATTERN = "Wrong pattern" private val CORRECT_PATTERN = FakeAuthenticationRepository.PATTERN } }