• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.view.View
22 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
23 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
24 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
25 import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
26 import com.android.systemui.res.R
27 import dagger.assisted.Assisted
28 import dagger.assisted.AssistedFactory
29 import dagger.assisted.AssistedInject
30 import kotlin.math.max
31 import kotlin.math.min
32 import kotlin.math.pow
33 import kotlin.math.sqrt
34 import kotlinx.coroutines.awaitCancellation
35 import kotlinx.coroutines.coroutineScope
36 import kotlinx.coroutines.flow.MutableStateFlow
37 import kotlinx.coroutines.flow.StateFlow
38 import kotlinx.coroutines.flow.asStateFlow
39 import kotlinx.coroutines.flow.map
40 import com.android.app.tracing.coroutines.launchTraced as launch
41 
42 /** Holds UI state and handles user input for the pattern bouncer UI. */
43 class PatternBouncerViewModel
44 @AssistedInject
45 constructor(
46     private val applicationContext: Context,
47     interactor: BouncerInteractor,
48     @Assisted bouncerHapticPlayer: BouncerHapticPlayer,
49     @Assisted isInputEnabled: StateFlow<Boolean>,
50     @Assisted private val onIntentionalUserInput: () -> Unit,
51 ) :
52     AuthMethodBouncerViewModel(
53         interactor = interactor,
54         isInputEnabled = isInputEnabled,
55         traceName = "PatternBouncerViewModel",
56         bouncerHapticPlayer = bouncerHapticPlayer,
57     ) {
58 
59     /** The number of columns in the dot grid. */
60     val columnCount = 3
61 
62     /** The number of rows in the dot grid. */
63     val rowCount = 3
64 
65     private val selectedDotSet = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf())
66     private val selectedDotList = MutableStateFlow(selectedDotSet.value.toList())
67     /** The dots that were selected by the user, in the order of selection. */
68     val selectedDots: StateFlow<List<PatternDotViewModel>> = selectedDotList.asStateFlow()
69 
70     private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null)
71 
72     /** The most-recently selected dot that the user selected. */
73     val currentDot: StateFlow<PatternDotViewModel?> = _currentDot.asStateFlow()
74 
75     private val _dots = MutableStateFlow(defaultDots())
76 
77     /** All dots on the grid. */
78     val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow()
79 
80     /** Whether the pattern itself should be rendered visibly. */
81     val isPatternVisible: StateFlow<Boolean> = interactor.isPatternVisible
82 
83     override val authenticationMethod = AuthenticationMethodModel.Pattern
84 
85     override val lockoutMessageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message
86 
87     override suspend fun onActivated(): Nothing {
88         coroutineScope {
89             launch { super.onActivated() }
90             launch {
91                 selectedDotSet.map { it.toList() }.collect { selectedDotList.value = it.toList() }
92             }
93             awaitCancellation()
94         }
95     }
96 
97     /** Notifies that the user has started a drag gesture across the dot grid. */
98     fun onDragStart() {
99         onIntentionalUserInput()
100     }
101 
102     /**
103      * Notifies that the user is dragging across the dot grid.
104      *
105      * @param xPx The horizontal coordinate of the position of the user's pointer, in pixels.
106      * @param yPx The vertical coordinate of the position of the user's pointer, in pixels.
107      * @param containerSizePx The size of the container of the dot grid, in pixels. It's assumed
108      *   that the dot grid is perfectly square such that width and height are equal.
109      */
110     fun onDrag(xPx: Float, yPx: Float, containerSizePx: Int) {
111         val cellWidthPx = containerSizePx / columnCount
112         val cellHeightPx = containerSizePx / rowCount
113 
114         if (xPx < 0 || yPx < 0) {
115             return
116         }
117 
118         val dotColumn = (xPx / cellWidthPx).toInt()
119         val dotRow = (yPx / cellHeightPx).toInt()
120         if (dotColumn > columnCount - 1 || dotRow > rowCount - 1) {
121             return
122         }
123 
124         val dotPixelX = dotColumn * cellWidthPx + cellWidthPx / 2
125         val dotPixelY = dotRow * cellHeightPx + cellHeightPx / 2
126 
127         val distance = sqrt((xPx - dotPixelX).pow(2) + (yPx - dotPixelY).pow(2))
128         val hitRadius = hitFactor * min(cellWidthPx, cellHeightPx) / 2
129         if (distance > hitRadius) {
130             return
131         }
132 
133         val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow }
134         if (hitDot != null && !selectedDotSet.value.contains(hitDot)) {
135             val skippedOverDots =
136                 currentDot.value?.let { previousDot ->
137                     buildList {
138                         var dot = previousDot
139                         while (dot != hitDot) {
140                             // Move along the direction of the line connecting the previously
141                             // selected dot and current hit dot, and see if they were skipped over
142                             // but fall on that line.
143                             if (dot.isOnLineSegment(previousDot, hitDot)) {
144                                 add(dot)
145                             }
146                             dot =
147                                 PatternDotViewModel(
148                                     x =
149                                         if (hitDot.x > dot.x) {
150                                             dot.x + 1
151                                         } else if (hitDot.x < dot.x) dot.x - 1 else dot.x,
152                                     y =
153                                         if (hitDot.y > dot.y) {
154                                             dot.y + 1
155                                         } else if (hitDot.y < dot.y) dot.y - 1 else dot.y,
156                                 )
157                         }
158                     }
159                 } ?: emptyList()
160 
161             selectedDotSet.value =
162                 linkedSetOf<PatternDotViewModel>().apply {
163                     addAll(selectedDotSet.value)
164                     addAll(skippedOverDots)
165                     add(hitDot)
166                 }
167             _currentDot.value = hitDot
168         }
169     }
170 
171     /** Notifies that the user has ended the drag gesture across the dot grid. */
172     fun onDragEnd() {
173         val pattern = getInput()
174         if (pattern.size == 1) {
175             // Single dot patterns are treated as erroneous/false taps:
176             interactor.onFalseUserInput()
177         }
178 
179         clearInput()
180         tryAuthenticate(input = pattern)
181     }
182 
183     override fun clearInput() {
184         _dots.value = defaultDots()
185         _currentDot.value = null
186         selectedDotSet.value = linkedSetOf()
187     }
188 
189     override fun getInput(): List<Any> {
190         return selectedDotSet.value.map(PatternDotViewModel::toCoordinate)
191     }
192 
193     private fun defaultDots(): List<PatternDotViewModel> {
194         return buildList {
195             (0 until columnCount).forEach { x ->
196                 (0 until rowCount).forEach { y -> add(PatternDotViewModel(x = x, y = y)) }
197             }
198         }
199     }
200 
201     private val hitFactor: Float by lazy {
202         val outValue = TypedValue()
203         applicationContext.resources.getValue(
204             com.android.internal.R.dimen.lock_pattern_dot_hit_factor,
205             outValue,
206             true,
207         )
208         max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR)
209     }
210 
211     fun performDotFeedback(view: View?) = bouncerHapticPlayer?.playPatternDotFeedback(view)
212 
213     @AssistedFactory
214     interface Factory {
215         fun create(
216             bouncerHapticPlayer: BouncerHapticPlayer,
217             isInputEnabled: StateFlow<Boolean>,
218             onIntentionalUserInput: () -> Unit,
219         ): PatternBouncerViewModel
220     }
221 
222     companion object {
223         private const val MIN_DOT_HIT_FACTOR = 0.2f
224     }
225 }
226 
227 /**
228  * Determines whether [this] dot is present on the line segment connecting [first] and [second]
229  * dots.
230  */
isOnLineSegmentnull231 private fun PatternDotViewModel.isOnLineSegment(
232     first: PatternDotViewModel,
233     second: PatternDotViewModel,
234 ): Boolean {
235     val anotherPoint = this
236     // No need to consider any points outside the bounds of two end points
237     val isWithinBounds =
238         anotherPoint.x.isBetween(first.x, second.x) && anotherPoint.y.isBetween(first.y, second.y)
239     if (!isWithinBounds) {
240         return false
241     }
242 
243     // Uses the 2 point line equation: (y-y1)/(x-x1) = (y2-y1)/(x2-x1)
244     // which can be rewritten as:      (y-y1)*(x2-x1) = (x-x1)*(y2-y1)
245     // This is true for any point on the line passing through these two points
246     return (anotherPoint.y - first.y) * (second.x - first.x) ==
247         (anotherPoint.x - first.x) * (second.y - first.y)
248 }
249 
250 /** Is [this] Int between [a] and [b] */
isBetweennull251 private fun Int.isBetween(a: Int, b: Int): Boolean {
252     return (this in a..b) || (this in b..a)
253 }
254 
255 data class PatternDotViewModel(val x: Int, val y: Int) {
toCoordinatenull256     fun toCoordinate(): AuthenticationPatternCoordinate {
257         return AuthenticationPatternCoordinate(x = x, y = y)
258     }
259 }
260