• 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     @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val input: List<EntryToken>
40 ) {
41     init {
<lambda>null42         require(input.firstOrNull() is ClearAll) { "input does not begin with a ClearAll token" }
<lambda>null43         require(input.zipWithNext().all { it.first < it.second }) {
44             "EntryTokens are not sorted by their sequenceNumber"
45         }
46     }
47     /**
48      * [PinInputViewModel] with [previousInput] and appended [newToken].
49      *
50      * [previousInput] is trimmed so that the new [PinBouncerViewModel] contains at most two pin
51      * inputs.
52      */
53     private constructor(
54         previousInput: List<EntryToken>,
55         newToken: EntryToken
56     ) : this(
<lambda>null57         buildList {
58             addAll(
59                 previousInput.subList(previousInput.indexOfLastClearAllToKeep(), previousInput.size)
60             )
61             add(newToken)
62         }
63     )
64 
appendnull65     fun append(digit: Int): PinInputViewModel {
66         return PinInputViewModel(input, Digit(digit))
67     }
68 
69     /**
70      * Delete last digit.
71      *
72      * This removes the last digit from the input. Returns `this` if the last token is [ClearAll].
73      */
deleteLastnull74     fun deleteLast(): PinInputViewModel {
75         if (isEmpty()) return this
76         return PinInputViewModel(input.take(input.size - 1))
77     }
78 
79     /**
80      * Appends a [ClearAll] watermark, completing the current pin.
81      *
82      * Returns `this` if the last token is [ClearAll].
83      */
clearAllnull84     fun clearAll(): PinInputViewModel {
85         if (isEmpty()) return this
86         return PinInputViewModel(input, ClearAll())
87     }
88 
89     /** Whether the current pin is empty. */
isEmptynull90     fun isEmpty(): Boolean {
91         return input.last() is ClearAll
92     }
93 
94     /** The current pin, or an empty list if [isEmpty]. */
getPinnull95     fun getPin(): List<Int> {
96         return getDigits(mostRecentClearAll()).map { it.input }
97     }
98 
99     /**
100      * The digits following the specified [ClearAll] marker, up to the next marker or the end of the
101      * input.
102      *
103      * Returns an empty list if the [ClearAll] is not in the input.
104      */
getDigitsnull105     fun getDigits(clearAllMarker: ClearAll): List<Digit> {
106         val startIndex = input.indexOf(clearAllMarker) + 1
107         if (startIndex == 0 || startIndex == input.size) return emptyList()
108 
109         return input.subList(startIndex, input.size).takeWhile { it is Digit }.map { it as Digit }
110     }
111 
112     /** The most recent [ClearAll] marker. */
mostRecentClearAllnull113     fun mostRecentClearAll(): ClearAll {
114         return input.last { it is ClearAll } as ClearAll
115     }
116 
117     companion object {
emptynull118         fun empty() = PinInputViewModel(listOf(ClearAll()))
119     }
120 }
121 
122 /**
123  * Pin bouncer entry token with a [sequenceNumber] to indicate input event ordering.
124  *
125  * Since the model only allows appending/removing [Digit]s from the end, the [sequenceNumber] is
126  * strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at a
127  * specific number.
128  */
129 sealed interface EntryToken : Comparable<EntryToken> {
130     val sequenceNumber: Int
131 
132     /** The pin bouncer [input] as digits 0-9. */
133     data class Digit(val input: Int, override val sequenceNumber: Int = nextSequenceNumber++) :
134         EntryToken {
135         init {
136             check(input in 0..9)
137         }
138     }
139 
140     /**
141      * Marker to indicate the input is completely cleared, and subsequent [EntryToken]s mark a new
142      * pin entry.
143      */
144     data class ClearAll(override val sequenceNumber: Int = nextSequenceNumber++) : EntryToken
145 
146     override fun compareTo(other: EntryToken): Int =
147         compareValuesBy(this, other, EntryToken::sequenceNumber)
148 
149     companion object {
150         private var nextSequenceNumber = 1
151     }
152 }
153 
154 /**
155  * Index of the last [ClearAll] token to keep for a new [PinInputViewModel], so that after appending
156  * another [EntryToken], there are at most two pin inputs in the [PinInputViewModel].
157  */
Listnull158 private fun List<EntryToken>.indexOfLastClearAllToKeep(): Int {
159     require(isNotEmpty() && first() is ClearAll)
160 
161     var seenClearAll = 0
162     for (i in size - 1 downTo 0) {
163         if (get(i) is ClearAll) {
164             seenClearAll++
165             if (seenClearAll == 2) {
166                 return i
167             }
168         }
169     }
170 
171     // The first element is guaranteed to be a ClearAll marker.
172     return 0
173 }
174