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