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