1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.bouncer.ui.viewmodel
18 
19 import android.content.Context
20 import android.util.TypedValue
21 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
22 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
23 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
24 import com.android.systemui.res.R
25 import kotlin.math.max
26 import kotlin.math.min
27 import kotlin.math.pow
28 import kotlin.math.sqrt
29 import kotlinx.coroutines.CoroutineScope
30 import kotlinx.coroutines.flow.MutableStateFlow
31 import kotlinx.coroutines.flow.SharingStarted
32 import kotlinx.coroutines.flow.StateFlow
33 import kotlinx.coroutines.flow.asStateFlow
34 import kotlinx.coroutines.flow.map
35 import kotlinx.coroutines.flow.stateIn
36 
37 /** Holds UI state and handles user input for the pattern bouncer UI. */
38 class PatternBouncerViewModel(
39     private val applicationContext: Context,
40     viewModelScope: CoroutineScope,
41     interactor: BouncerInteractor,
42     isInputEnabled: StateFlow<Boolean>,
43     private val onIntentionalUserInput: () -> Unit,
44 ) :
45     AuthMethodBouncerViewModel(
46         viewModelScope = viewModelScope,
47         interactor = interactor,
48         isInputEnabled = isInputEnabled,
49     ) {
50 
51     /** The number of columns in the dot grid. */
52     val columnCount = 3
53 
54     /** The number of rows in the dot grid. */
55     val rowCount = 3
56 
57     private val _selectedDots = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf())
58 
59     /** The dots that were selected by the user, in the order of selection. */
60     val selectedDots: StateFlow<List<PatternDotViewModel>> =
61         _selectedDots
62             .map { it.toList() }
63             .stateIn(
64                 scope = viewModelScope,
65                 started = SharingStarted.WhileSubscribed(),
66                 initialValue = emptyList(),
67             )
68 
69     private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null)
70 
71     /** The most-recently selected dot that the user selected. */
72     val currentDot: StateFlow<PatternDotViewModel?> = _currentDot.asStateFlow()
73 
74     private val _dots = MutableStateFlow(defaultDots())
75 
76     /** All dots on the grid. */
77     val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow()
78 
79     /** Whether the pattern itself should be rendered visibly. */
80     val isPatternVisible: StateFlow<Boolean> = interactor.isPatternVisible
81 
82     override val authenticationMethod = AuthenticationMethodModel.Pattern
83 
84     override val lockoutMessageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message
85 
86     /** Notifies that the user has started a drag gesture across the dot grid. */
87     fun onDragStart() {
88         onIntentionalUserInput()
89     }
90 
91     /**
92      * Notifies that the user is dragging across the dot grid.
93      *
94      * @param xPx The horizontal coordinate of the position of the user's pointer, in pixels.
95      * @param yPx The vertical coordinate of the position of the user's pointer, in pixels.
96      * @param containerSizePx The size of the container of the dot grid, in pixels. It's assumed
97      *   that the dot grid is perfectly square such that width and height are equal.
98      */
99     fun onDrag(xPx: Float, yPx: Float, containerSizePx: Int) {
100         val cellWidthPx = containerSizePx / columnCount
101         val cellHeightPx = containerSizePx / rowCount
102 
103         if (xPx < 0 || yPx < 0) {
104             return
105         }
106 
107         val dotColumn = (xPx / cellWidthPx).toInt()
108         val dotRow = (yPx / cellHeightPx).toInt()
109         if (dotColumn > columnCount - 1 || dotRow > rowCount - 1) {
110             return
111         }
112 
113         val dotPixelX = dotColumn * cellWidthPx + cellWidthPx / 2
114         val dotPixelY = dotRow * cellHeightPx + cellHeightPx / 2
115 
116         val distance = sqrt((xPx - dotPixelX).pow(2) + (yPx - dotPixelY).pow(2))
117         val hitRadius = hitFactor * min(cellWidthPx, cellHeightPx) / 2
118         if (distance > hitRadius) {
119             return
120         }
121 
122         val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow }
123         if (hitDot != null && !_selectedDots.value.contains(hitDot)) {
124             val skippedOverDots =
125                 currentDot.value?.let { previousDot ->
126                     buildList {
127                         var dot = previousDot
128                         while (dot != hitDot) {
129                             // Move along the direction of the line connecting the previously
130                             // selected dot and current hit dot, and see if they were skipped over
131                             // but fall on that line.
132                             if (dot.isOnLineSegment(previousDot, hitDot)) {
133                                 add(dot)
134                             }
135                             dot =
136                                 PatternDotViewModel(
137                                     x =
138                                         if (hitDot.x > dot.x) {
139                                             dot.x + 1
140                                         } else if (hitDot.x < dot.x) dot.x - 1 else dot.x,
141                                     y =
142                                         if (hitDot.y > dot.y) {
143                                             dot.y + 1
144                                         } else if (hitDot.y < dot.y) dot.y - 1 else dot.y,
145                                 )
146                         }
147                     }
148                 }
149                     ?: emptyList()
150 
151             _selectedDots.value =
152                 linkedSetOf<PatternDotViewModel>().apply {
153                     addAll(_selectedDots.value)
154                     addAll(skippedOverDots)
155                     add(hitDot)
156                 }
157             _currentDot.value = hitDot
158         }
159     }
160 
161     /** Notifies that the user has ended the drag gesture across the dot grid. */
162     fun onDragEnd() {
163         val pattern = getInput()
164         if (pattern.size == 1) {
165             // Single dot patterns are treated as erroneous/false taps:
166             interactor.onFalseUserInput()
167         }
168 
169         clearInput()
170         tryAuthenticate(input = pattern)
171     }
172 
173     override fun clearInput() {
174         _dots.value = defaultDots()
175         _currentDot.value = null
176         _selectedDots.value = linkedSetOf()
177     }
178 
179     override fun getInput(): List<Any> {
180         return _selectedDots.value.map(PatternDotViewModel::toCoordinate)
181     }
182 
183     private fun defaultDots(): List<PatternDotViewModel> {
184         return buildList {
185             (0 until columnCount).forEach { x ->
186                 (0 until rowCount).forEach { y ->
187                     add(
188                         PatternDotViewModel(
189                             x = x,
190                             y = y,
191                         )
192                     )
193                 }
194             }
195         }
196     }
197 
198     private val hitFactor: Float by lazy {
199         val outValue = TypedValue()
200         applicationContext.resources.getValue(
201             com.android.internal.R.dimen.lock_pattern_dot_hit_factor,
202             outValue,
203             true
204         )
205         max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR)
206     }
207 
208     companion object {
209         private const val MIN_DOT_HIT_FACTOR = 0.2f
210     }
211 }
212 
213 /**
214  * Determines whether [this] dot is present on the line segment connecting [first] and [second]
215  * dots.
216  */
isOnLineSegmentnull217 private fun PatternDotViewModel.isOnLineSegment(
218     first: PatternDotViewModel,
219     second: PatternDotViewModel
220 ): Boolean {
221     val anotherPoint = this
222     // No need to consider any points outside the bounds of two end points
223     val isWithinBounds =
224         anotherPoint.x.isBetween(first.x, second.x) && anotherPoint.y.isBetween(first.y, second.y)
225     if (!isWithinBounds) {
226         return false
227     }
228 
229     // Uses the 2 point line equation: (y-y1)/(x-x1) = (y2-y1)/(x2-x1)
230     // which can be rewritten as:      (y-y1)*(x2-x1) = (x-x1)*(y2-y1)
231     // This is true for any point on the line passing through these two points
232     return (anotherPoint.y - first.y) * (second.x - first.x) ==
233         (anotherPoint.x - first.x) * (second.y - first.y)
234 }
235 
236 /** Is [this] Int between [a] and [b] */
isBetweennull237 private fun Int.isBetween(a: Int, b: Int): Boolean {
238     return (this in a..b) || (this in b..a)
239 }
240 
241 data class PatternDotViewModel(
242     val x: Int,
243     val y: Int,
244 ) {
toCoordinatenull245     fun toCoordinate(): AuthenticationPatternCoordinate {
246         return AuthenticationPatternCoordinate(
247             x = x,
248             y = y,
249         )
250     }
251 }
252