• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.annotation.VisibleForTesting
20 import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll
21 import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
22 
23 /**
24  * Immutable pin input state.
25  *
26  * The input is a hybrid of state ([Digit]) and event ([ClearAll]) tokens. The [ClearAll] token can
27  * be interpreted as a watermark, indicating that the current input up to that point is deleted
28  * (after a auth failure or when long-pressing the delete button). Therefore, [Digit]s following a
29  * [ClearAll] make up the next pin input entry. Up to two complete pin inputs are memoized.
30  *
31  * This is required when auto-confirm rejects the input, and the last digit will be animated-in at
32  * the end of the input, concurrently with the staggered clear-all animation starting to play at the
33  * beginning of the input.
34  *
35  * The input is guaranteed to always contain a initial [ClearAll] token as a sentinel, thus clients
36  * can always assume there is a 'ClearAll' watermark available.
37  */
38 data class PinInputViewModel
39 internal constructor(
40     @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
41     internal val input: List<EntryToken>
42 ) {
43     init {
<lambda>null44         require(input.firstOrNull() is ClearAll) { "input does not begin with a ClearAll token" }
<lambda>null45         require(input.zipWithNext().all { it.first < it.second }) {
46             "EntryTokens are not sorted by their sequenceNumber"
47         }
48     }
49     /**
50      * [PinInputViewModel] with [previousInput] and appended [newToken].
51      *
52      * [previousInput] is trimmed so that the new [PinBouncerViewModel] contains at most two pin
53      * inputs.
54      */
55     private constructor(
56         previousInput: List<EntryToken>,
57         newToken: EntryToken
58     ) : this(
<lambda>null59         buildList {
60             addAll(
61                 previousInput.subList(previousInput.indexOfLastClearAllToKeep(), previousInput.size)
62             )
63             add(newToken)
64         }
65     )
66 
appendnull67     fun append(digit: Int): PinInputViewModel {
68         return PinInputViewModel(input, Digit(digit))
69     }
70 
71     /**
72      * Delete last digit.
73      *
74      * This removes the last digit from the input. Returns `this` if the last token is [ClearAll].
75      */
deleteLastnull76     fun deleteLast(): PinInputViewModel {
77         if (isEmpty()) return this
78         return PinInputViewModel(input.take(input.size - 1))
79     }
80 
81     /**
82      * Appends a [ClearAll] watermark, completing the current pin.
83      *
84      * Returns `this` if the last token is [ClearAll].
85      */
clearAllnull86     fun clearAll(): PinInputViewModel {
87         if (isEmpty()) return this
88         return PinInputViewModel(input, ClearAll())
89     }
90 
91     /** Whether the current pin is empty. */
isEmptynull92     fun isEmpty(): Boolean {
93         return input.last() is ClearAll
94     }
95 
96     /** The current pin, or an empty list if [isEmpty]. */
getPinnull97     fun getPin(): List<Int> {
98         return getDigits(mostRecentClearAll()).map { it.input }
99     }
100 
101     /**
102      * The digits following the specified [ClearAll] marker, up to the next marker or the end of the
103      * input.
104      *
105      * Returns an empty list if the [ClearAll] is not in the input.
106      */
getDigitsnull107     fun getDigits(clearAllMarker: ClearAll): List<Digit> {
108         val startIndex = input.indexOf(clearAllMarker) + 1
109         if (startIndex == 0 || startIndex == input.size) return emptyList()
110 
111         return input.subList(startIndex, input.size).takeWhile { it is Digit }.map { it as Digit }
112     }
113 
114     /** The most recent [ClearAll] marker. */
mostRecentClearAllnull115     fun mostRecentClearAll(): ClearAll {
116         return input.last { it is ClearAll } as ClearAll
117     }
118 
119     companion object {
emptynull120         fun empty() = PinInputViewModel(listOf(ClearAll()))
121     }
122 }
123 
124 /**
125  * Pin bouncer entry token with a [sequenceNumber] to indicate input event ordering.
126  *
127  * Since the model only allows appending/removing [Digit]s from the end, the [sequenceNumber] is
128  * strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at a
129  * specific number.
130  */
131 sealed interface EntryToken : Comparable<EntryToken> {
132     val sequenceNumber: Int
133 
134     /** The pin bouncer [input] as digits 0-9. */
135     data class Digit
136     internal constructor(val input: Int, override val sequenceNumber: Int = nextSequenceNumber++) :
137         EntryToken {
138         init {
139             check(input in 0..9)
140         }
141     }
142 
143     /**
144      * Marker to indicate the input is completely cleared, and subsequent [EntryToken]s mark a new
145      * pin entry.
146      */
147     data class ClearAll
148     internal constructor(override val sequenceNumber: Int = nextSequenceNumber++) : EntryToken
149 
150     override fun compareTo(other: EntryToken): Int =
151         compareValuesBy(this, other, EntryToken::sequenceNumber)
152 
153     companion object {
154         private var nextSequenceNumber = 1
155     }
156 }
157 
158 /**
159  * Index of the last [ClearAll] token to keep for a new [PinInputViewModel], so that after appending
160  * another [EntryToken], there are at most two pin inputs in the [PinInputViewModel].
161  */
Listnull162 private fun List<EntryToken>.indexOfLastClearAllToKeep(): Int {
163     require(isNotEmpty() && first() is ClearAll)
164 
165     var seenClearAll = 0
166     for (i in size - 1 downTo 0) {
167         if (get(i) is ClearAll) {
168             seenClearAll++
169             if (seenClearAll == 2) {
170                 return i
171             }
172         }
173     }
174 
175     // The first element is guaranteed to be a ClearAll marker.
176     return 0
177 }
178