Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 858078c3 authored by Chandru S's avatar Chandru S
Browse files

Fix pattern snapping to incorrect dots.

Check if the dot is on the line segment connecting previous dot and the currently hit dot before adding it to the skipped over dots list.

Fixes: 299343202
Test: atest PatternBouncerViewModelTest
Test: verified manually, 1-2-3-4 pattern should work without snapping to 1-2-3-5-4

Change-Id: I5e98d883796209af52c606450ddf1c6d2f100dd1
parent b1eb819a
Loading
Loading
Loading
Loading
+34 −1
Original line number Diff line number Diff line
@@ -129,7 +129,12 @@ class PatternBouncerViewModel(
                    buildList {
                        var dot = previousDot
                        while (dot != hitDot) {
                            // Move along the direction of the line connecting the previously
                            // selected dot and current hit dot, and see if they were skipped over
                            // but fall on that line.
                            if (dot.isOnLineSegment(previousDot, hitDot)) {
                                add(dot)
                            }
                            dot =
                                PatternDotViewModel(
                                    x =
@@ -208,6 +213,34 @@ class PatternBouncerViewModel(
    }
}

/**
 * Determines whether [this] dot is present on the line segment connecting [first] and [second]
 * dots.
 */
private fun PatternDotViewModel.isOnLineSegment(
    first: PatternDotViewModel,
    second: PatternDotViewModel
): Boolean {
    val anotherPoint = this
    // No need to consider any points outside the bounds of two end points
    val isWithinBounds =
        anotherPoint.x.isBetween(first.x, second.x) && anotherPoint.y.isBetween(first.y, second.y)
    if (!isWithinBounds) {
        return false
    }

    // Uses the 2 point line equation: (y-y1)/(x-x1) = (y2-y1)/(x2-x1)
    // which can be rewritten as:      (y-y1)*(x2-x1) = (x-x1)*(y2-y1)
    // This is true for any point on the line passing through these two points
    return (anotherPoint.y - first.y) * (second.x - first.x) ==
        (anotherPoint.x - first.x) * (second.y - first.y)
}

/** Is [this] Int between [a] and [b] */
private fun Int.isBetween(a: Int, b: Int): Boolean {
    return (this in a..b) || (this in b..a)
}

data class PatternDotViewModel(
    val x: Int,
    val y: Int,
+178 −65
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
@@ -30,6 +31,7 @@ import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -67,6 +69,9 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            isInputEnabled = MutableStateFlow(true).asStateFlow(),
        )

    private val containerSize = 90 // px
    private val dotSize = 30 // px

    @Before
    fun setUp() {
        overrideResource(R.string.keyguard_enter_your_pattern, ENTER_YOUR_PATTERN)
@@ -80,13 +85,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            val message by collectLastValue(bouncerViewModel.message)
            val selectedDots by collectLastValue(underTest.selectedDots)
            val currentDot by collectLastValue(underTest.currentDot)
            utils.authenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pattern
            )
            utils.authenticationRepository.setUnlocked(false)
            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
            transitionToPatternBouncer()

            underTest.onShown()

@@ -103,13 +102,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            val message by collectLastValue(bouncerViewModel.message)
            val selectedDots by collectLastValue(underTest.selectedDots)
            val currentDot by collectLastValue(underTest.currentDot)
            utils.authenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pattern
            )
            utils.authenticationRepository.setUnlocked(false)
            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
            transitionToPatternBouncer()
            underTest.onShown()
            runCurrent()

@@ -127,23 +120,12 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            val currentScene by collectLastValue(sceneInteractor.desiredScene)
            val selectedDots by collectLastValue(underTest.selectedDots)
            val currentDot by collectLastValue(underTest.currentDot)
            utils.authenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pattern
            )
            utils.authenticationRepository.setUnlocked(false)
            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
            transitionToPatternBouncer()
            underTest.onShown()
            underTest.onDragStart()
            assertThat(currentDot).isNull()
            CORRECT_PATTERN.forEachIndexed { index, coordinate ->
                underTest.onDrag(
                    xPx = 30f * coordinate.x + 15,
                    yPx = 30f * coordinate.y + 15,
                    containerSizePx = 90,
                    verticalOffsetPx = 0f,
                )
                dragToCoordinate(coordinate)
                assertWithMessage("Wrong selected dots for index $index")
                    .that(selectedDots)
                    .isEqualTo(
@@ -176,23 +158,10 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            val message by collectLastValue(bouncerViewModel.message)
            val selectedDots by collectLastValue(underTest.selectedDots)
            val currentDot by collectLastValue(underTest.currentDot)
            utils.authenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pattern
            )
            utils.authenticationRepository.setUnlocked(false)
            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
            transitionToPatternBouncer()
            underTest.onShown()
            underTest.onDragStart()
            CORRECT_PATTERN.subList(0, 3).forEach { coordinate ->
                underTest.onDrag(
                    xPx = 30f * coordinate.x + 15,
                    yPx = 30f * coordinate.y + 15,
                    containerSizePx = 90,
                    verticalOffsetPx = 0f,
                )
            }
            CORRECT_PATTERN.subList(0, 3).forEach { coordinate -> dragToCoordinate(coordinate) }

            underTest.onDragEnd()

@@ -202,6 +171,147 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
        }

    @Test
    fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheSameRow() =
        testScope.runTest {
            val selectedDots by collectLastValue(underTest.selectedDots)
            transitionToPatternBouncer()
            underTest.onShown()

            /*
             * 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() =
        testScope.runTest {
            val selectedDots by collectLastValue(underTest.selectedDots)
            transitionToPatternBouncer()
            underTest.onShown()

            /*
             * 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() =
        testScope.runTest {
            val selectedDots by collectLastValue(underTest.selectedDots)
            transitionToPatternBouncer()
            underTest.onShown()

            /*
             * 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() =
        testScope.runTest {
            val selectedDots by collectLastValue(underTest.selectedDots)
            transitionToPatternBouncer()
            underTest.onShown()

            /*
             * 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() =
        testScope.runTest {
            val selectedDots by collectLastValue(underTest.selectedDots)
            transitionToPatternBouncer()
            underTest.onShown()

            /*
             * 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() =
        testScope.runTest {
@@ -252,23 +362,10 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            val message by collectLastValue(bouncerViewModel.message)
            val selectedDots by collectLastValue(underTest.selectedDots)
            val currentDot by collectLastValue(underTest.currentDot)
            utils.authenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pattern
            )
            utils.authenticationRepository.setUnlocked(false)
            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
            transitionToPatternBouncer()
            underTest.onShown()
            underTest.onDragStart()
            CORRECT_PATTERN.subList(2, 7).forEach { coordinate ->
                underTest.onDrag(
                    xPx = 30f * coordinate.x + 15,
                    yPx = 30f * coordinate.y + 15,
                    containerSizePx = 90,
                    verticalOffsetPx = 0f,
                )
            }
            CORRECT_PATTERN.subList(2, 7).forEach { coordinate -> dragToCoordinate(coordinate) }
            underTest.onDragEnd()
            assertThat(selectedDots).isEmpty()
            assertThat(currentDot).isNull()
@@ -276,18 +373,34 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))

            // Enter the correct pattern:
            CORRECT_PATTERN.forEach { coordinate ->
            CORRECT_PATTERN.forEach { coordinate -> dragToCoordinate(coordinate) }

            underTest.onDragEnd()

            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
        }

    private fun dragOverCoordinates(vararg coordinatesDragged: Point) {
        underTest.onDragStart()
        coordinatesDragged.forEach { dragToCoordinate(it) }
    }

    private fun dragToCoordinate(coordinate: Point) {
        underTest.onDrag(
                    xPx = 30f * coordinate.x + 15,
                    yPx = 30f * coordinate.y + 15,
                    containerSizePx = 90,
            xPx = dotSize * coordinate.x + 15f,
            yPx = dotSize * coordinate.y + 15f,
            containerSizePx = containerSize,
            verticalOffsetPx = 0f,
        )
    }

            underTest.onDragEnd()

            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
    private fun TestScope.transitionToPatternBouncer() {
        utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern)
        utils.authenticationRepository.setUnlocked(false)
        sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
        sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
        assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
            .isEqualTo(SceneModel(SceneKey.Bouncer))
    }

    companion object {