1 /* 2 * 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.AuthenticationPatternCoordinate 22 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 23 import kotlin.math.max 24 import kotlin.math.min 25 import kotlin.math.pow 26 import kotlin.math.sqrt 27 import kotlinx.coroutines.CoroutineScope 28 import kotlinx.coroutines.flow.MutableStateFlow 29 import kotlinx.coroutines.flow.SharingStarted 30 import kotlinx.coroutines.flow.StateFlow 31 import kotlinx.coroutines.flow.asStateFlow 32 import kotlinx.coroutines.flow.map 33 import kotlinx.coroutines.flow.stateIn 34 import kotlinx.coroutines.launch 35 36 /** Holds UI state and handles user input for the pattern bouncer UI. */ 37 class PatternBouncerViewModel( 38 private val applicationContext: Context, 39 private val applicationScope: CoroutineScope, 40 private val interactor: BouncerInteractor, 41 isInputEnabled: StateFlow<Boolean>, 42 ) : 43 AuthMethodBouncerViewModel( 44 isInputEnabled = isInputEnabled, 45 ) { 46 47 /** The number of columns in the dot grid. */ 48 val columnCount = 3 49 /** The number of rows in the dot grid. */ 50 val rowCount = 3 51 52 private val _selectedDots = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf()) 53 /** The dots that were selected by the user, in the order of selection. */ 54 val selectedDots: StateFlow<List<PatternDotViewModel>> = 55 _selectedDots 56 .map { it.toList() } 57 .stateIn( 58 scope = applicationScope, 59 started = SharingStarted.WhileSubscribed(), 60 initialValue = emptyList(), 61 ) 62 63 private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null) 64 /** The most-recently selected dot that the user selected. */ 65 val currentDot: StateFlow<PatternDotViewModel?> = _currentDot.asStateFlow() 66 67 private val _dots = MutableStateFlow(defaultDots()) 68 /** All dots on the grid. */ 69 val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow() 70 71 /** Whether the pattern itself should be rendered visibly. */ 72 val isPatternVisible: StateFlow<Boolean> = interactor.isPatternVisible 73 74 /** Notifies that the UI has been shown to the user. */ 75 fun onShown() { 76 interactor.resetMessage() 77 } 78 79 /** Notifies that the user has started a drag gesture across the dot grid. */ 80 fun onDragStart() { 81 interactor.clearMessage() 82 } 83 84 /** 85 * Notifies that the user is dragging across the dot grid. 86 * 87 * @param xPx The horizontal coordinate of the position of the user's pointer, in pixels. 88 * @param yPx The vertical coordinate of the position of the user's pointer, in pixels. 89 * @param containerSizePx The size of the container of the dot grid, in pixels. It's assumed 90 * that the dot grid is perfectly square such that width and height are equal. 91 * @param verticalOffsetPx How far down from `0` does the dot grid start on the display. 92 */ 93 fun onDrag(xPx: Float, yPx: Float, containerSizePx: Int, verticalOffsetPx: Float) { 94 val cellWidthPx = containerSizePx / columnCount 95 val cellHeightPx = containerSizePx / rowCount 96 97 if (xPx < 0 || yPx < verticalOffsetPx) { 98 return 99 } 100 101 val dotColumn = (xPx / cellWidthPx).toInt() 102 val dotRow = ((yPx - verticalOffsetPx) / cellHeightPx).toInt() 103 if (dotColumn > columnCount - 1 || dotRow > rowCount - 1) { 104 return 105 } 106 107 val dotPixelX = dotColumn * cellWidthPx + cellWidthPx / 2 108 val dotPixelY = dotRow * cellHeightPx + cellHeightPx / 2 + verticalOffsetPx 109 110 val distance = sqrt((xPx - dotPixelX).pow(2) + (yPx - dotPixelY).pow(2)) 111 val hitRadius = hitFactor * min(cellWidthPx, cellHeightPx) / 2 112 if (distance > hitRadius) { 113 return 114 } 115 116 val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow } 117 if (hitDot != null && !_selectedDots.value.contains(hitDot)) { 118 val skippedOverDots = 119 currentDot.value?.let { previousDot -> 120 buildList { 121 var dot = previousDot 122 while (dot != hitDot) { 123 add(dot) 124 dot = 125 PatternDotViewModel( 126 x = 127 if (hitDot.x > dot.x) dot.x + 1 128 else if (hitDot.x < dot.x) dot.x - 1 else dot.x, 129 y = 130 if (hitDot.y > dot.y) dot.y + 1 131 else if (hitDot.y < dot.y) dot.y - 1 else dot.y, 132 ) 133 } 134 } 135 } 136 ?: emptyList() 137 138 _selectedDots.value = 139 linkedSetOf<PatternDotViewModel>().apply { 140 addAll(_selectedDots.value) 141 addAll(skippedOverDots) 142 add(hitDot) 143 } 144 _currentDot.value = hitDot 145 } 146 } 147 148 /** Notifies that the user has ended the drag gesture across the dot grid. */ 149 fun onDragEnd() { 150 val pattern = _selectedDots.value.map { it.toCoordinate() } 151 _dots.value = defaultDots() 152 _currentDot.value = null 153 _selectedDots.value = linkedSetOf() 154 155 applicationScope.launch { 156 if (interactor.authenticate(pattern) != true) { 157 showFailureAnimation() 158 } 159 } 160 } 161 162 private fun defaultDots(): List<PatternDotViewModel> { 163 return buildList { 164 (0 until columnCount).forEach { x -> 165 (0 until rowCount).forEach { y -> 166 add( 167 PatternDotViewModel( 168 x = x, 169 y = y, 170 ) 171 ) 172 } 173 } 174 } 175 } 176 177 private val hitFactor: Float by lazy { 178 val outValue = TypedValue() 179 applicationContext.resources.getValue( 180 com.android.internal.R.dimen.lock_pattern_dot_hit_factor, 181 outValue, 182 true 183 ) 184 max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR) 185 } 186 187 companion object { 188 private const val MIN_DOT_HIT_FACTOR = 0.2f 189 } 190 } 191 192 data class PatternDotViewModel( 193 val x: Int, 194 val y: Int, 195 ) { 196 fun toCoordinate(): AuthenticationPatternCoordinate { 197 return AuthenticationPatternCoordinate( 198 x = x, 199 y = y, 200 ) 201 } 202 } 203