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
18 package com.android.systemui.biometrics.ui.binder
19
20 import android.graphics.drawable.AnimatedVectorDrawable
21 import android.util.Log
22 import androidx.constraintlayout.widget.ConstraintLayout
23 import androidx.constraintlayout.widget.ConstraintSet
24 import androidx.lifecycle.Lifecycle
25 import androidx.lifecycle.repeatOnLifecycle
26 import com.airbnb.lottie.LottieAnimationView
27 import com.airbnb.lottie.LottieOnCompositionLoadedListener
28 import com.airbnb.lottie.LottieListener
29 import com.android.settingslib.widget.LottieColorUtils
30 import com.android.systemui.Flags.constraintBp
31 import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel
32 import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType
33 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
34 import com.android.systemui.lifecycle.repeatWhenAttached
35 import com.android.systemui.res.R
36 import com.android.systemui.util.kotlin.Utils.Companion.toQuad
37 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
38 import com.android.systemui.util.kotlin.sample
39 import kotlinx.coroutines.flow.combine
40 import kotlinx.coroutines.launch
41
42 private const val TAG = "PromptIconViewBinder"
43
44 /** Sub-binder for [BiometricPromptLayout.iconView]. */
45 object PromptIconViewBinder {
46 /**
47 * Binds [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay] to
48 * [PromptIconViewModel].
49 */
50 @JvmStatic
51 fun bind(
52 iconView: LottieAnimationView,
53 iconOverlayView: LottieAnimationView,
54 iconViewLayoutParamSizeOverride: Pair<Int, Int>?,
55 promptViewModel: PromptViewModel
56 ) {
57 val viewModel = promptViewModel.iconViewModel
58 iconView.repeatWhenAttached {
59 repeatOnLifecycle(Lifecycle.State.STARTED) {
60 viewModel.onConfigurationChanged(iconView.context.resources.configuration)
61 if (iconViewLayoutParamSizeOverride != null) {
62 iconView.layoutParams.width = iconViewLayoutParamSizeOverride.first
63 iconView.layoutParams.height = iconViewLayoutParamSizeOverride.second
64
65 iconOverlayView.layoutParams.width = iconViewLayoutParamSizeOverride.first
66 iconOverlayView.layoutParams.height = iconViewLayoutParamSizeOverride.second
67 }
68
69 var faceIcon: AnimatedVectorDrawable? = null
70
71 fun updateXmlIconAsset(
72 iconAsset: Int,
73 shouldAnimateIconView: Boolean,
74 activeAuthType: AuthType
75 ) {
76 faceIcon?.stop()
77 faceIcon = iconView.context.getDrawable(iconAsset) as AnimatedVectorDrawable
78 faceIcon?.apply {
79 iconView.setIconFailureListener(iconAsset, activeAuthType)
80 iconView.setImageDrawable(this)
81 if (shouldAnimateIconView) {
82 forceAnimationOnUI()
83 start()
84 }
85 }
86 }
87
88 fun updateJsonIconAsset(
89 iconAsset: Int,
90 shouldAnimateIconView: Boolean,
91 activeAuthType: AuthType
92 ) {
93 iconView.setIconFailureListener(iconAsset, activeAuthType)
94 iconView.setAnimation(iconAsset)
95 iconView.frame = 0
96
97 if (shouldAnimateIconView) {
98 iconView.playAnimation()
99 }
100 }
101
102 if (!constraintBp()) {
103 launch {
104 var lottieOnCompositionLoadedListener: LottieOnCompositionLoadedListener? =
105 null
106
107 combine(viewModel.activeAuthType, viewModel.iconSize, ::Pair).collect {
108 (activeAuthType, iconSize) ->
109 // Every time after bp shows, [isIconViewLoaded] is set to false in
110 // [BiometricViewSizeBinder]. Then when biometric prompt view is redrew
111 // (when size or activeAuthType changes), we need to update
112 // [isIconViewLoaded] here to keep it correct.
113 when (activeAuthType) {
114 AuthType.Fingerprint,
115 AuthType.Coex -> {
116 /**
117 * View is only set visible in BiometricViewSizeBinder once
118 * PromptSize is determined that accounts for iconView size, to
119 * prevent prompt resizing being visible to the user.
120 *
121 * TODO(b/288175072): May be able to remove this once constraint
122 * layout is implemented
123 */
124 if (lottieOnCompositionLoadedListener != null) {
125 iconView.removeLottieOnCompositionLoadedListener(
126 lottieOnCompositionLoadedListener!!
127 )
128 }
129 lottieOnCompositionLoadedListener =
130 LottieOnCompositionLoadedListener {
131 promptViewModel.setIsIconViewLoaded(true)
132 }
133 iconView.addLottieOnCompositionLoadedListener(
134 lottieOnCompositionLoadedListener!!
135 )
136 }
137 AuthType.Face -> {
138 /**
139 * Set to true by default since face icon is a drawable, which
140 * doesn't have a LottieOnCompositionLoadedListener equivalent.
141 *
142 * TODO(b/318569643): To be updated once face assets are updated
143 * from drawables
144 */
145 promptViewModel.setIsIconViewLoaded(true)
146 }
147 }
148
149 if (iconViewLayoutParamSizeOverride == null) {
150 iconView.layoutParams.width = iconSize.first
151 iconView.layoutParams.height = iconSize.second
152
153 iconOverlayView.layoutParams.width = iconSize.first
154 iconOverlayView.layoutParams.height = iconSize.second
155 }
156 }
157 }
158 }
159
160 launch {
161 viewModel.iconAsset
162 .sample(
163 combine(
164 viewModel.activeAuthType,
165 viewModel.shouldAnimateIconView,
166 viewModel.showingError,
167 ::Triple
168 ),
169 ::toQuad
170 )
171 .collect { (iconAsset, activeAuthType, shouldAnimateIconView, showingError)
172 ->
173 if (iconAsset != -1) {
174 when (activeAuthType) {
175 AuthType.Fingerprint,
176 AuthType.Coex -> {
177 // TODO(b/318569643): Until assets unified to one type, this
178 // check
179 // is needed in face-auth-error-triggered implicit ->
180 // explicit
181 // coex auth transition, in case iconAsset updates to
182 // face_dialog_dark_to_error (XML) after activeAuthType
183 // updates
184 // from AuthType.Face (which expects XML)
185 // to AuthType.Coex (which expects JSON)
186 if (iconAsset == R.drawable.face_dialog_dark_to_error) {
187 updateXmlIconAsset(
188 iconAsset,
189 shouldAnimateIconView,
190 activeAuthType
191 )
192 } else {
193 updateJsonIconAsset(
194 iconAsset,
195 shouldAnimateIconView,
196 activeAuthType
197 )
198 }
199 }
200 AuthType.Face -> {
201 // TODO(b/318569643): Consolidate logic once all face auth
202 // assets are migrated from drawable to json
203 if (iconAsset == R.raw.face_dialog_authenticating) {
204 updateJsonIconAsset(
205 iconAsset,
206 shouldAnimateIconView,
207 activeAuthType
208 )
209 } else {
210 updateXmlIconAsset(
211 iconAsset,
212 shouldAnimateIconView,
213 activeAuthType
214 )
215 }
216 }
217 }
218 LottieColorUtils.applyDynamicColors(iconView.context, iconView)
219 viewModel.setPreviousIconWasError(showingError)
220 }
221 }
222 }
223
224 launch {
225 viewModel.iconOverlayAsset
226 .sample(
227 combine(
228 viewModel.shouldAnimateIconOverlay,
229 viewModel.showingError,
230 ::Pair
231 ),
232 ::toTriple
233 )
234 .collect { (iconOverlayAsset, shouldAnimateIconOverlay, showingError) ->
235 if (iconOverlayAsset != -1) {
236 iconOverlayView.setIconOverlayFailureListener(iconOverlayAsset)
237 iconOverlayView.setAnimation(iconOverlayAsset)
238 iconOverlayView.frame = 0
239 LottieColorUtils.applyDynamicColors(
240 iconOverlayView.context,
241 iconOverlayView
242 )
243
244 if (shouldAnimateIconOverlay) {
245 iconOverlayView.playAnimation()
246 }
247 viewModel.setPreviousIconOverlayWasError(showingError)
248 }
249 }
250 }
251
252 launch {
253 viewModel.shouldFlipIconView.collect { shouldFlipIconView ->
254 if (shouldFlipIconView) {
255 iconView.rotation = 180f
256 }
257 }
258 }
259
260 launch {
261 viewModel.contentDescriptionId.collect { id ->
262 if (id != -1) {
263 iconView.contentDescription = iconView.context.getString(id)
264 }
265 }
266 }
267 }
268 }
269 }
270 }
271
272 private val assetIdToString: Map<Int, String> =
273 mapOf(
274 // UDFPS assets
275 R.raw.fingerprint_dialogue_error_to_fingerprint_lottie to
276 "fingerprint_dialogue_error_to_fingerprint_lottie",
277 R.raw.fingerprint_dialogue_error_to_success_lottie to
278 "fingerprint_dialogue_error_to_success_lottie",
279 R.raw.fingerprint_dialogue_fingerprint_to_error_lottie to
280 "fingerprint_dialogue_fingerprint_to_error_lottie",
281 R.raw.fingerprint_dialogue_fingerprint_to_success_lottie to
282 "fingerprint_dialogue_fingerprint_to_success_lottie",
283 // SFPS assets
284 R.raw.biometricprompt_fingerprint_to_error_landscape to
285 "biometricprompt_fingerprint_to_error_landscape",
286 R.raw.biometricprompt_folded_base_bottomright to "biometricprompt_folded_base_bottomright",
287 R.raw.biometricprompt_folded_base_default to "biometricprompt_folded_base_default",
288 R.raw.biometricprompt_folded_base_topleft to "biometricprompt_folded_base_topleft",
289 R.raw.biometricprompt_landscape_base to "biometricprompt_landscape_base",
290 R.raw.biometricprompt_portrait_base_bottomright to
291 "biometricprompt_portrait_base_bottomright",
292 R.raw.biometricprompt_portrait_base_topleft to "biometricprompt_portrait_base_topleft",
293 R.raw.biometricprompt_symbol_error_to_fingerprint_landscape to
294 "biometricprompt_symbol_error_to_fingerprint_landscape",
295 R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright to
296 "biometricprompt_symbol_error_to_fingerprint_portrait_bottomright",
297 R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft to
298 "biometricprompt_symbol_error_to_fingerprint_portrait_topleft",
299 R.raw.biometricprompt_symbol_error_to_success_landscape to
300 "biometricprompt_symbol_error_to_success_landscape",
301 R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright to
302 "biometricprompt_symbol_error_to_success_portrait_bottomright",
303 R.raw.biometricprompt_symbol_error_to_success_portrait_topleft to
304 "biometricprompt_symbol_error_to_success_portrait_topleft",
305 R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright to
306 "biometricprompt_symbol_fingerprint_to_error_portrait_bottomright",
307 R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft to
308 "biometricprompt_symbol_fingerprint_to_error_portrait_topleft",
309 R.raw.biometricprompt_symbol_fingerprint_to_success_landscape to
310 "biometricprompt_symbol_fingerprint_to_success_landscape",
311 R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright to
312 "biometricprompt_symbol_fingerprint_to_success_portrait_bottomright",
313 R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft to
314 "biometricprompt_symbol_fingerprint_to_success_portrait_topleft",
315 // Face assets
316 R.drawable.face_dialog_wink_from_dark to "face_dialog_wink_from_dark",
317 R.drawable.face_dialog_dark_to_checkmark to "face_dialog_dark_to_checkmark",
318 R.drawable.face_dialog_dark_to_error to "face_dialog_dark_to_error",
319 R.drawable.face_dialog_error_to_idle to "face_dialog_error_to_idle",
320 R.drawable.face_dialog_idle_static to "face_dialog_idle_static",
321 R.raw.face_dialog_authenticating to "face_dialog_authenticating",
322 // Co-ex assets
323 R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie to
324 "fingerprint_dialogue_unlocked_to_checkmark_success_lottie",
325 R.raw.fingerprint_dialogue_error_to_unlock_lottie to
326 "fingerprint_dialogue_error_to_unlock_lottie",
327 R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie to
328 "fingerprint_dialogue_fingerprint_to_unlock_lottie",
329 )
330
getAssetNameFromIdnull331 private fun getAssetNameFromId(id: Int): String {
332 return assetIdToString.getOrDefault(id, "Asset $id not found")
333 }
334
LottieAnimationViewnull335 private fun LottieAnimationView.setIconFailureListener(iconAsset: Int, activeAuthType: AuthType) {
336 setFailureListener(
337 LottieListener<Throwable> { result: Throwable? ->
338 Log.d(
339 TAG,
340 "Collecting iconAsset | " +
341 "activeAuthType = $activeAuthType | " +
342 "Invalid resource id: $iconAsset, " +
343 "name ${getAssetNameFromId(iconAsset)}",
344 result
345 )
346 }
347 )
348 }
349
setIconOverlayFailureListenernull350 private fun LottieAnimationView.setIconOverlayFailureListener(iconOverlayAsset: Int) {
351 setFailureListener(
352 LottieListener<Throwable> { result: Throwable? ->
353 Log.d(
354 TAG,
355 "Collecting iconOverlayAsset | " +
356 "Invalid resource id: $iconOverlayAsset, " +
357 "name ${getAssetNameFromId(iconOverlayAsset)}",
358 result
359 )
360 }
361 )
362 }
363