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 package com.android.settings.biometrics2.ui.view
17 
18 import android.animation.Animator
19 import android.animation.ObjectAnimator
20 import android.annotation.RawRes
21 import android.content.Context
22 import android.content.res.ColorStateList
23 import android.content.res.Configuration
24 import android.graphics.PorterDuff
25 import android.graphics.PorterDuffColorFilter
26 import android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL
27 import android.os.Bundle
28 import android.util.Log
29 import android.view.LayoutInflater
30 import android.view.MotionEvent
31 import android.view.View
32 import android.view.ViewGroup
33 import android.view.animation.AccelerateDecelerateInterpolator
34 import android.view.animation.AnimationUtils
35 import android.view.animation.Interpolator
36 import android.widget.ProgressBar
37 import android.widget.RelativeLayout
38 import androidx.activity.OnBackPressedCallback
39 import androidx.fragment.app.Fragment
40 import androidx.fragment.app.FragmentActivity
41 import androidx.lifecycle.Lifecycle
42 import androidx.lifecycle.Observer
43 import androidx.lifecycle.ViewModelProvider
44 import androidx.lifecycle.lifecycleScope
45 import androidx.lifecycle.repeatOnLifecycle
46 import com.airbnb.lottie.LottieAnimationView
47 import com.airbnb.lottie.LottieComposition
48 import com.airbnb.lottie.LottieCompositionFactory
49 import com.airbnb.lottie.LottieProperty
50 import com.airbnb.lottie.model.KeyPath
51 import com.android.settings.R
52 import com.android.settings.biometrics2.ui.model.EnrollmentProgress
53 import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
54 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
55 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
56 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
57 import com.google.android.setupcompat.template.FooterBarMixin
58 import com.google.android.setupcompat.template.FooterButton
59 import com.google.android.setupdesign.GlifLayout
60 import com.google.android.setupdesign.template.DescriptionMixin
61 import com.google.android.setupdesign.template.HeaderMixin
62 import kotlin.math.roundToInt
63 import kotlinx.coroutines.launch
64 
65 /**
66  * Fragment is used to handle enrolling process for sfps
67  */
68 class FingerprintEnrollEnrollingSfpsFragment : Fragment() {
69 
70     private var _enrollingViewModel: FingerprintEnrollEnrollingViewModel? = null
71     private val enrollingViewModel: FingerprintEnrollEnrollingViewModel
72         get() = _enrollingViewModel!!
73 
74     private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
75     private val progressViewModel: FingerprintEnrollProgressViewModel
76         get() = _progressViewModel!!
77 
78     private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
79     private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
80         get() = _errorDialogViewModel!!
81 
82     private val fastOutSlowInInterpolator: Interpolator
83         get() = AnimationUtils.loadInterpolator(
84             activity,
85             androidx.appcompat.R.interpolator.fast_out_slow_in,
86         )
87 
88     private var enrollingView: GlifLayout? = null
89 
90     private val progressBar: ProgressBar
91         get() = enrollingView!!.findViewById(R.id.fingerprint_progress_bar)!!
92 
93     private var progressAnim: ObjectAnimator? = null
94 
95     private val progressAnimationListener: Animator.AnimatorListener =
96         object : Animator.AnimatorListener {
97             override fun onAnimationStart(animation: Animator) {}
98             override fun onAnimationRepeat(animation: Animator) {}
99             override fun onAnimationEnd(animation: Animator) {
100                 if (progressBar.progress >= PROGRESS_BAR_MAX) {
101                     progressBar.postDelayed(delayedFinishRunnable, PROGRESS_ANIMATION_DURATION)
102                 }
103             }
104 
105             override fun onAnimationCancel(animation: Animator) {}
106         }
107 
108     private val illustrationLottie: LottieAnimationView
109         get() = enrollingView!!.findViewById(R.id.illustration_lottie)!!
110 
111     private var haveShownSfpsNoAnimationLottie = false
112     private var haveShownSfpsCenterLottie = false
113     private var haveShownSfpsTipLottie = false
114     private var haveShownSfpsLeftEdgeLottie = false
115     private var haveShownSfpsRightEdgeLottie = false
116 
117     private var helpAnimation: ObjectAnimator? = null
118 
119     private var iconTouchCount = 0
120 
121     private val showIconTouchDialogRunnable = Runnable { showIconTouchDialog() }
122 
123     private var enrollingCancelSignal: Any? = null
124 
125     // Give the user a chance to see progress completed before jumping to the next stage.
126     private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
127 
128     private val onSkipClickListener = View.OnClickListener { _: View? ->
129         enrollingViewModel.setOnSkipPressed()
130         cancelEnrollment(true)
131     }
132 
133     private val progressObserver = Observer { progress: EnrollmentProgress? ->
134         if (progress != null && progress.steps >= 0) {
135             onEnrollmentProgressChange(progress)
136         }
137     }
138 
139     private val helpMessageObserver = Observer { helpMessage: EnrollmentStatusMessage? ->
140         helpMessage?.let { onEnrollmentHelp(it) }
141     }
142 
143     private val errorMessageObserver = Observer { errorMessage: EnrollmentStatusMessage? ->
144         Log.d(TAG, "errorMessageObserver($errorMessage)")
145         errorMessage?.let { onEnrollmentError(it) }
146     }
147 
148     private val canceledSignalObserver = Observer { canceledSignal: Any? ->
149         Log.d(TAG, "canceledSignalObserver($canceledSignal)")
150         canceledSignal?.let { onEnrollmentCanceled(it) }
151     }
152 
153     private val onBackPressedCallback: OnBackPressedCallback =
154         object : OnBackPressedCallback(true) {
155             override fun handleOnBackPressed() {
156                 isEnabled = false
157                 enrollingViewModel.setOnBackPressed()
158                 cancelEnrollment(true)
159             }
160         }
161 
162     override fun onAttach(context: Context) {
163         ViewModelProvider(requireActivity()).let { provider ->
164             _enrollingViewModel = provider[FingerprintEnrollEnrollingViewModel::class.java]
165             _progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
166             _errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
167         }
168         super.onAttach(context)
169         requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
170     }
171 
172     override fun onDetach() {
173         onBackPressedCallback.isEnabled = false
174         super.onDetach()
175     }
176 
177     override fun onCreateView(
178         inflater: LayoutInflater, container: ViewGroup?,
179         savedInstanceState: Bundle?
180     ): View? {
181         enrollingView = inflater.inflate(
182             R.layout.sfps_enroll_enrolling,
183             container, false
184         ) as GlifLayout
185         return enrollingView
186     }
187 
188     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
189         super.onViewCreated(view, savedInstanceState)
190 
191         requireActivity().bindFingerprintEnrollEnrollingSfpsView(
192             view = enrollingView!!,
193             onSkipClickListener = onSkipClickListener
194         )
195 
196         // setHelpAnimation()
197         helpAnimation = ObjectAnimator.ofFloat(
198             enrollingView!!.findViewById<RelativeLayout>(R.id.progress_lottie)!!,
199             "translationX" /* propertyName */,
200             0f,
201             HELP_ANIMATION_TRANSLATION_X,
202             -1 * HELP_ANIMATION_TRANSLATION_X,
203             HELP_ANIMATION_TRANSLATION_X,
204             0f
205         ).also {
206             it.interpolator = AccelerateDecelerateInterpolator()
207             it.setDuration(HELP_ANIMATION_DURATION)
208             it.setAutoCancel(false)
209         }
210 
211         progressBar.setOnTouchListener { _: View?, event: MotionEvent ->
212             if (event.actionMasked == MotionEvent.ACTION_DOWN) {
213                 iconTouchCount++
214                 if (iconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
215                     showIconTouchDialog()
216                 } else {
217                     progressBar.postDelayed(
218                         showIconTouchDialogRunnable,
219                         ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN
220                     )
221                 }
222             } else if (event.actionMasked == MotionEvent.ACTION_CANCEL
223                 || event.actionMasked == MotionEvent.ACTION_UP
224             ) {
225                 progressBar.removeCallbacks(showIconTouchDialogRunnable)
226             }
227             true
228         }
229 
230         lifecycleScope.launch {
231             repeatOnLifecycle(Lifecycle.State.STARTED) {
232                 errorDialogViewModel.triggerRetryFlow.collect { retryEnrollment() }
233             }
234         }
235     }
236 
237     private fun retryEnrollment() {
238         startEnrollment()
239         updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
240     }
241 
242     override fun onStart() {
243         super.onStart()
244         val isEnrolling = progressViewModel.isEnrolling
245         val isErrorDialogShown = errorDialogViewModel.isDialogShown
246         Log.d(TAG, "onStart(), isEnrolling:$isEnrolling, isErrorDialog:$isErrorDialogShown")
247         if (!isErrorDialogShown) {
248             startEnrollment()
249         }
250 
251         updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
252         progressViewModel.helpMessageLiveData.value.let {
253             if (it != null) {
254                 onEnrollmentHelp(it)
255             } else {
256                 clearError()
257                 updateTitleAndDescription()
258             }
259         }
260     }
261 
262     override fun onStop() {
263         removeEnrollmentObservers()
264         val isEnrolling = progressViewModel.isEnrolling
265         val isConfigChange = requireActivity().isChangingConfigurations
266         Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
267         if (isEnrolling && !isConfigChange) {
268             cancelEnrollment(false)
269         }
270         super.onStop()
271     }
272 
273     private fun removeEnrollmentObservers() {
274         progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
275         progressViewModel.progressLiveData.removeObserver(progressObserver)
276         progressViewModel.helpMessageLiveData.removeObserver(helpMessageObserver)
277     }
278 
279     private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
280         if (!progressViewModel.isEnrolling) {
281             Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
282             return
283         }
284         removeEnrollmentObservers()
285         if (waitForLastCancelErrMsg) {
286             progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
287         } else {
288             enrollingCancelSignal = null
289         }
290         val cancelResult: Boolean = progressViewModel.cancelEnrollment()
291         if (!cancelResult) {
292             Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
293         }
294     }
295 
296     private fun startEnrollment() {
297         enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_ENROLL)
298         if (enrollingCancelSignal == null) {
299             Log.e(TAG, "startEnrollment(), failed")
300         } else {
301             Log.d(TAG, "startEnrollment(), success")
302         }
303         progressViewModel.progressLiveData.observe(this, progressObserver)
304         progressViewModel.helpMessageLiveData.observe(this, helpMessageObserver)
305         progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
306     }
307 
308     private fun configureEnrollmentStage(description: CharSequence, @RawRes lottie: Int) {
309         GlifLayoutHelper(requireActivity(), enrollingView!!).setDescriptionText(description)
310         LottieCompositionFactory.fromRawRes(activity, lottie)
311             .addListener { c: LottieComposition ->
312                 illustrationLottie.setComposition(c)
313                 illustrationLottie.visibility = View.VISIBLE
314                 illustrationLottie.playAnimation()
315             }
316     }
317 
318     private val currentSfpsStage: Int
319         get() {
320             val progressLiveData: EnrollmentProgress =
321                 progressViewModel.progressLiveData.value
322                     ?: return STAGE_UNKNOWN
323             val progressSteps: Int = progressLiveData.steps - progressLiveData.remaining
324             return if (progressSteps < getStageThresholdSteps(0)) {
325                 SFPS_STAGE_NO_ANIMATION
326             } else if (progressSteps < getStageThresholdSteps(1)) {
327                 SFPS_STAGE_CENTER
328             } else if (progressSteps < getStageThresholdSteps(2)) {
329                 SFPS_STAGE_FINGERTIP
330             } else if (progressSteps < getStageThresholdSteps(3)) {
331                 SFPS_STAGE_LEFT_EDGE
332             } else {
333                 SFPS_STAGE_RIGHT_EDGE
334             }
335         }
336 
337     private fun onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
338         Log.d(TAG, "onEnrollmentHelp($helpMessage)")
339         val helpStr: CharSequence = helpMessage.str
340         if (helpStr.isNotEmpty()) {
341             showError(helpStr)
342         }
343     }
344 
345     private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
346         cancelEnrollment(true)
347         lifecycleScope.launch {
348             Log.d(TAG, "newDialog $errorMessage")
349             errorDialogViewModel.newDialog(errorMessage.msgId)
350         }
351     }
352 
353     private fun onEnrollmentCanceled(canceledSignal: Any) {
354         Log.d(
355             TAG,
356             "onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
357         )
358         if (enrollingCancelSignal === canceledSignal) {
359             progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
360             progressViewModel.clearProgressLiveData()
361             if (enrollingViewModel.onBackPressed) {
362                 enrollingViewModel.onCancelledDueToOnBackPressed()
363             } else if (enrollingViewModel.onSkipPressed) {
364                 enrollingViewModel.onCancelledDueToOnSkipPressed()
365             }
366         }
367     }
368 
369     private fun announceEnrollmentProgress(announcement: CharSequence) {
370         enrollingViewModel.sendAccessibilityEvent(announcement)
371     }
372 
373     private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
374         updateProgress(true /* animate */, progress)
375         if (enrollingViewModel.isAccessibilityEnabled) {
376             val percent: Int =
377                 ((progress.steps - progress.remaining).toFloat() / progress.steps.toFloat() * 100).toInt()
378             val announcement: CharSequence = getString(
379                 R.string.security_settings_sfps_enroll_progress_a11y_message, percent
380             )
381             announceEnrollmentProgress(announcement)
382             illustrationLottie.contentDescription =
383                 getString(R.string.security_settings_sfps_animation_a11y_label, percent)
384         }
385         updateTitleAndDescription()
386     }
387 
388     private fun updateProgress(animate: Boolean, enrollmentProgress: EnrollmentProgress) {
389         if (!progressViewModel.isEnrolling) {
390             Log.d(TAG, "Enrollment not started yet")
391             return
392         }
393 
394         val progress = getProgress(enrollmentProgress)
395         Log.d(TAG, "updateProgress($animate, $enrollmentProgress), old:${progressBar.progress}"
396                 + ", new:$progress")
397 
398         // Only clear the error when progress has been made.
399         // TODO (b/234772728) Add tests.
400         if (progressBar.progress < progress) {
401             clearError()
402         }
403         if (animate) {
404             animateProgress(progress)
405         } else {
406             progressBar.progress = progress
407             if (progress >= PROGRESS_BAR_MAX) {
408                 delayedFinishRunnable.run()
409             }
410         }
411     }
412 
413     private fun getProgress(progress: EnrollmentProgress): Int {
414         if (progress.steps == -1) {
415             return 0
416         }
417         val displayProgress = 0.coerceAtLeast(progress.steps + 1 - progress.remaining)
418         return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
419     }
420 
421     private fun showError(error: CharSequence) {
422         enrollingView!!.let {
423             it.headerText = error
424             it.headerTextView.contentDescription = error
425             GlifLayoutHelper(requireActivity(), it).setDescriptionText("")
426         }
427 
428         if (isResumed && !helpAnimation!!.isRunning) {
429             helpAnimation!!.start()
430         }
431         applySfpsErrorDynamicColors(true)
432         if (isResumed && enrollingViewModel.isAccessibilityEnabled) {
433             enrollingViewModel.vibrateError(javaClass.simpleName + "::showError")
434         }
435     }
436 
437     private fun clearError() {
438         applySfpsErrorDynamicColors(false)
439     }
440 
441     private fun animateProgress(progress: Int) {
442         progressAnim?.cancel()
443         progressAnim = ObjectAnimator.ofInt(
444             progressBar,
445             "progress",
446             progressBar.progress,
447             progress
448         ).also {
449             it.addListener(progressAnimationListener)
450             it.interpolator = fastOutSlowInInterpolator
451             it.setDuration(PROGRESS_ANIMATION_DURATION)
452             it.start()
453         }
454     }
455 
456     /**
457      * Applies dynamic colors corresponding to showing or clearing errors on the progress bar
458      * and finger lottie for SFPS
459      */
460     private fun applySfpsErrorDynamicColors(isError: Boolean) {
461         progressBar.applyProgressBarDynamicColor(requireContext(), isError)
462         illustrationLottie.applyLottieDynamicColor(requireContext(), isError)
463     }
464 
465     private fun getStageThresholdSteps(index: Int): Int {
466         val progressLiveData: EnrollmentProgress? =
467             progressViewModel.progressLiveData.value
468         if (progressLiveData == null || progressLiveData.steps == -1) {
469             Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet")
470             return 1
471         }
472         return (progressLiveData.steps
473                 * enrollingViewModel.getEnrollStageThreshold(index)).roundToInt()
474     }
475 
476     private fun updateTitleAndDescription() {
477         val helper = GlifLayoutHelper(requireActivity(), enrollingView!!)
478         if (enrollingViewModel.isAccessibilityEnabled) {
479             enrollingViewModel.clearTalkback()
480             helper.glifLayout.descriptionTextView.accessibilityLiveRegion =
481                 View.ACCESSIBILITY_LIVE_REGION_POLITE
482         }
483         val stage = currentSfpsStage
484         if (DEBUG) {
485             Log.d(
486                 TAG, "updateTitleAndDescription, stage:" + stage
487                         + ", noAnimation:" + haveShownSfpsNoAnimationLottie
488                         + ", center:" + haveShownSfpsCenterLottie
489                         + ", tip:" + haveShownSfpsTipLottie
490                         + ", leftEdge:" + haveShownSfpsLeftEdgeLottie
491                         + ", rightEdge:" + haveShownSfpsRightEdgeLottie
492             )
493         }
494         when (stage) {
495             SFPS_STAGE_NO_ANIMATION -> {
496                 helper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title)
497                 if (!haveShownSfpsNoAnimationLottie) {
498                     haveShownSfpsNoAnimationLottie = true
499                     illustrationLottie.contentDescription =
500                         getString(R.string.security_settings_sfps_animation_a11y_label, 0)
501                     configureEnrollmentStage(
502                         getString(R.string.security_settings_sfps_enroll_start_message),
503                         R.raw.sfps_lottie_no_animation
504                     )
505                 }
506             }
507 
508             SFPS_STAGE_CENTER -> {
509                 helper.setHeaderText(R.string.security_settings_sfps_enroll_finger_center_title)
510                 if (!haveShownSfpsCenterLottie) {
511                     haveShownSfpsCenterLottie = true
512                     configureEnrollmentStage(
513                         getString(R.string.security_settings_sfps_enroll_start_message),
514                         R.raw.sfps_lottie_pad_center
515                     )
516                 }
517             }
518 
519             SFPS_STAGE_FINGERTIP -> {
520                 helper.setHeaderText(R.string.security_settings_sfps_enroll_fingertip_title)
521                 if (!haveShownSfpsTipLottie) {
522                     haveShownSfpsTipLottie = true
523                     configureEnrollmentStage("", R.raw.sfps_lottie_tip)
524                 }
525             }
526 
527             SFPS_STAGE_LEFT_EDGE -> {
528                 helper.setHeaderText(R.string.security_settings_sfps_enroll_left_edge_title)
529                 if (!haveShownSfpsLeftEdgeLottie) {
530                     haveShownSfpsLeftEdgeLottie = true
531                     configureEnrollmentStage("", R.raw.sfps_lottie_left_edge)
532                 }
533             }
534 
535             SFPS_STAGE_RIGHT_EDGE -> {
536                 helper.setHeaderText(R.string.security_settings_sfps_enroll_right_edge_title)
537                 if (!haveShownSfpsRightEdgeLottie) {
538                     haveShownSfpsRightEdgeLottie = true
539                     configureEnrollmentStage("", R.raw.sfps_lottie_right_edge)
540                 }
541             }
542 
543             STAGE_UNKNOWN -> {
544                 // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
545                 // which gets announced for a11y upon entering the page. For SFPS, we want to
546                 // announce a different string for a11y upon entering the page.
547                 helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
548                 helper.setDescriptionText(
549                     getString(R.string.security_settings_sfps_enroll_start_message)
550                 )
551                 val description: CharSequence = getString(
552                     R.string.security_settings_sfps_enroll_find_sensor_message
553                 )
554                 helper.glifLayout.headerTextView.contentDescription = description
555                 helper.activity.title = description
556             }
557 
558             else -> {
559                 helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
560                 helper.setDescriptionText(
561                     getString(R.string.security_settings_sfps_enroll_start_message)
562                 )
563                 val description: CharSequence = getString(
564                     R.string.security_settings_sfps_enroll_find_sensor_message
565                 )
566                 helper.glifLayout.headerTextView.contentDescription = description
567                 helper.activity.title = description
568             }
569         }
570     }
571 
572     private fun showIconTouchDialog() {
573         iconTouchCount = 0
574         enrollingViewModel.showIconTouchDialog()
575     }
576 
577     companion object {
578         private val TAG = FingerprintEnrollEnrollingSfpsFragment::class.java.simpleName
579         private const val DEBUG = false
580         private const val PROGRESS_BAR_MAX = 10000
581         private const val HELP_ANIMATION_DURATION = 550L
582         private const val HELP_ANIMATION_TRANSLATION_X = 40f
583         private const val PROGRESS_ANIMATION_DURATION = 250L
584         private const val ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN: Long = 500
585         private const val ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3
586         private const val STAGE_UNKNOWN = -1
587         private const val SFPS_STAGE_NO_ANIMATION = 0
588         private const val SFPS_STAGE_CENTER = 1
589         private const val SFPS_STAGE_FINGERTIP = 2
590         private const val SFPS_STAGE_LEFT_EDGE = 3
591         private const val SFPS_STAGE_RIGHT_EDGE = 4
592     }
593 }
594 
bindFingerprintEnrollEnrollingSfpsViewnull595 fun FragmentActivity.bindFingerprintEnrollEnrollingSfpsView(
596     view: GlifLayout,
597     onSkipClickListener: View.OnClickListener
598 ) {
599     GlifLayoutHelper(this, view).setDescriptionText(
600         getString(R.string.security_settings_fingerprint_enroll_start_message)
601     )
602 
603     view.getMixin(FooterBarMixin::class.java).secondaryButton = FooterButton.Builder(this)
604         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
605         .setListener(onSkipClickListener)
606         .setButtonType(FooterButton.ButtonType.SKIP)
607         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
608         .build()
609 
610     view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!.progressBackgroundTintMode =
611         PorterDuff.Mode.SRC
612 
613     view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
614         .applyProgressBarDynamicColor(this, false)
615 
616     view.findViewById<LottieAnimationView>(R.id.illustration_lottie)!!
617         .applyLottieDynamicColor(this, false)
618 
619     view.maybeHideSfpsText(resources.configuration.orientation)
620 }
621 
applyProgressBarDynamicColornull622 private fun ProgressBar.applyProgressBarDynamicColor(context: Context, isError: Boolean) {
623     progressTintList = ColorStateList.valueOf(
624         context.getColor(
625             if (isError)
626                 R.color.sfps_enrollment_progress_bar_error_color
627             else
628                 R.color.sfps_enrollment_progress_bar_fill_color
629         )
630     )
631     progressTintMode = PorterDuff.Mode.SRC
632     invalidate()
633 }
634 
applyLottieDynamicColornull635 fun LottieAnimationView.applyLottieDynamicColor(context: Context, isError: Boolean) {
636     addValueCallback(
637         KeyPath(".blue100", "**"),
638         LottieProperty.COLOR_FILTER
639     ) {
640         PorterDuffColorFilter(
641             context.getColor(
642                 if (isError)
643                     R.color.sfps_enrollment_fp_error_color
644                 else
645                     R.color.sfps_enrollment_fp_captured_color
646             ),
647             PorterDuff.Mode.SRC_ATOP
648         )
649     }
650     invalidate()
651 }
652 
GlifLayoutnull653 fun GlifLayout.maybeHideSfpsText(@Configuration.Orientation orientation: Int) {
654     val headerMixin: HeaderMixin = getMixin(HeaderMixin::class.java)
655     val descriptionMixin: DescriptionMixin = getMixin(DescriptionMixin::class.java)
656 
657     val isLandscape = (orientation == Configuration.ORIENTATION_LANDSCAPE)
658     headerMixin.setAutoTextSizeEnabled(isLandscape)
659     if (isLandscape) {
660         headerMixin.textView.minLines = 0
661         headerMixin.textView.maxLines = 10
662         descriptionMixin.textView.minLines = 0
663         descriptionMixin.textView.maxLines = 10
664     } else {
665         headerMixin.textView.setLines(4)
666         // hide the description
667         descriptionMixin.textView.setLines(0)
668     }
669 }
670