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.annotation.RawRes
19 import android.content.Context
20 import android.hardware.biometrics.BiometricFingerprintConstants
21 import android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL
22 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
23 import android.os.Bundle
24 import android.util.Log
25 import android.view.LayoutInflater
26 import android.view.Surface
27 import android.view.Surface.ROTATION_270
28 import android.view.Surface.ROTATION_90
29 import android.view.View
30 import android.view.ViewGroup
31 import android.widget.Button
32 import android.widget.ImageView
33 import android.widget.RelativeLayout
34 import android.widget.TextView
35 import androidx.activity.OnBackPressedCallback
36 import androidx.fragment.app.Fragment
37 import androidx.fragment.app.FragmentActivity
38 import androidx.lifecycle.Lifecycle
39 import androidx.lifecycle.MutableLiveData
40 import androidx.lifecycle.Observer
41 import androidx.lifecycle.ViewModelProvider
42 import androidx.lifecycle.lifecycleScope
43 import androidx.lifecycle.repeatOnLifecycle
44 import com.airbnb.lottie.LottieAnimationView
45 import com.airbnb.lottie.LottieComposition
46 import com.airbnb.lottie.LottieCompositionFactory
47 import com.android.settings.R
48 import com.android.settings.biometrics2.ui.model.EnrollmentProgress
49 import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
50 import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel
51 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
52 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
53 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
54 import com.android.settings.biometrics2.ui.widget.UdfpsEnrollView
55 import com.android.settingslib.display.DisplayDensityUtils
56 import kotlinx.coroutines.launch
57 import kotlin.math.roundToInt
58 
59 /**
60  * Fragment is used to handle enrolling process for udfps
61  */
62 class FingerprintEnrollEnrollingUdfpsFragment : Fragment() {
63 
64     private var _enrollingViewModel: FingerprintEnrollEnrollingViewModel? = null
65     private val enrollingViewModel: FingerprintEnrollEnrollingViewModel
66         get() = _enrollingViewModel!!
67 
68     private var _rotationViewModel: DeviceRotationViewModel? = null
69     private val rotationViewModel: DeviceRotationViewModel
70         get() = _rotationViewModel!!
71 
72     private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
73     private val progressViewModel: FingerprintEnrollProgressViewModel
74         get() = _progressViewModel!!
75 
76     private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
77     private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
78         get() = _errorDialogViewModel!!
79 
80     private var illustrationLottie: LottieAnimationView? = null
81 
82     private var haveShownTipLottie = false
83     private var haveShownLeftEdgeLottie = false
84     private var haveShownRightEdgeLottie = false
85     private var haveShownCenterLottie = false
86     private var haveShownGuideLottie = false
87 
88     private var enrollingView: RelativeLayout? = null
89 
90     private val titleText: TextView
91         get() = enrollingView!!.findViewById(R.id.suc_layout_title)!!
92 
93     private val subTitleText: TextView
94         get() = enrollingView!!.findViewById(R.id.sud_layout_subtitle)!!
95 
96     private val udfpsEnrollView: UdfpsEnrollView
97         get() = enrollingView!!.findViewById(R.id.udfps_animation_view)!!
98 
99     private val skipBtn: Button
100         get() = enrollingView!!.findViewById(R.id.skip_btn)!!
101 
102     private val icon: ImageView
103         get() = enrollingView!!.findViewById(R.id.sud_layout_icon)!!
104 
105     private val shouldShowLottie: Boolean
106         get() {
107             val displayDensity = DisplayDensityUtils(requireContext())
108             val currentDensityIndex: Int = displayDensity.currentIndexForDefaultDisplay
109             val currentDensity: Int =
110                 displayDensity.defaultDisplayDensityValues[currentDensityIndex]
111             val defaultDensity: Int = displayDensity.defaultDensityForDefaultDisplay
112             return defaultDensity == currentDensity
113         }
114 
115     private val isAccessibilityEnabled
116         get() = enrollingViewModel.isAccessibilityEnabled
117 
118     private var rotation = -1
119 
120     private var enrollingCancelSignal: Any? = null
121 
122     private val onSkipClickListener = View.OnClickListener { _: View? ->
123         enrollingViewModel.setOnSkipPressed()
124         cancelEnrollment(true) // TODO Add test after b/273640000 fixed
125     }
126 
127     private val progressObserver = Observer { progress: EnrollmentProgress? ->
128         if (progress != null && progress.steps >= 0) {
129             onEnrollmentProgressChange(progress)
130         }
131     }
132 
133     private val helpMessageObserver = Observer { helpMessage: EnrollmentStatusMessage? ->
134         Log.d(TAG, "helpMessageObserver($helpMessage)")
135         helpMessage?.let { onEnrollmentHelp(it) }
136     }
137 
138     private val errorMessageObserver = Observer { errorMessage: EnrollmentStatusMessage? ->
139         Log.d(TAG, "errorMessageObserver($errorMessage)")
140         errorMessage?.let { onEnrollmentError(it) }
141     }
142 
143     private val canceledSignalObserver = Observer { canceledSignal: Any? ->
144         Log.d(TAG, "canceledSignalObserver($canceledSignal)")
145         canceledSignal?.let { onEnrollmentCanceled(it) }
146     }
147 
148     private val acquireObserver =
149         Observer { isAcquiredGood: Boolean? -> isAcquiredGood?.let { onAcquired(it) } }
150 
151     private val pointerDownObserver =
152         Observer { sensorId: Int? -> sensorId?.let { onPointerDown(it) } }
153 
154     private val pointerUpObserver =
155         Observer { sensorId: Int? -> sensorId?.let { onPointerUp(it) } }
156 
157     private val rotationObserver =
158         Observer { rotation: Int? -> rotation?.let { onRotationChanged(it) } }
159 
160     private val onBackPressedCallback: OnBackPressedCallback =
161         object : OnBackPressedCallback(true) {
162             override fun handleOnBackPressed() {
163                 isEnabled = false
164                 enrollingViewModel.setOnBackPressed()
165                 cancelEnrollment(true)
166             }
167         }
168 
169     // Give the user a chance to see progress completed before jumping to the next stage.
170     private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
171 
172     override fun onAttach(context: Context) {
173         ViewModelProvider(requireActivity()).let { provider ->
174             _enrollingViewModel = provider[FingerprintEnrollEnrollingViewModel::class.java]
175             _rotationViewModel = provider[DeviceRotationViewModel::class.java]
176             _progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
177             _errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
178         }
179         super.onAttach(context)
180         requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
181     }
182 
183     override fun onDetach() {
184         onBackPressedCallback.isEnabled = false
185         super.onDetach()
186     }
187 
188     override fun onCreateView(
189         inflater: LayoutInflater, container: ViewGroup?,
190         savedInstanceState: Bundle?
191     ): View = (inflater.inflate(
192         R.layout.udfps_enroll_enrolling_v2, container, false
193     ) as RelativeLayout).also {
194         enrollingView = it
195     }
196 
197     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
198         super.onViewCreated(view, savedInstanceState)
199         rotation = rotationViewModel.liveData.value!!
200         updateIllustrationLottie(rotation)
201 
202         requireActivity().bindFingerprintEnrollEnrollingUdfpsView(
203             view = enrollingView!!,
204             sensorProperties = enrollingViewModel.firstFingerprintSensorPropertiesInternal!!,
205             rotation = rotation,
206             onSkipClickListener = onSkipClickListener,
207         )
208 
209         lifecycleScope.launch {
210             repeatOnLifecycle(Lifecycle.State.STARTED) {
211                 errorDialogViewModel.triggerRetryFlow.collect { retryEnrollment() }
212             }
213         }
214     }
215 
216     private fun retryEnrollment() {
217         reattachUdfpsEnrollView()
218 
219         startEnrollment()
220 
221         updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
222         progressViewModel.helpMessageLiveData.value.let {
223             if (it != null) {
224                 onEnrollmentHelp(it)
225             } else {
226                 updateTitleAndDescription()
227             }
228         }
229     }
230 
231     override fun onStart() {
232         super.onStart()
233         val isEnrolling = progressViewModel.isEnrolling
234         val isErrorDialogShown = errorDialogViewModel.isDialogShown
235         Log.d(TAG, "onStart(), isEnrolling:$isEnrolling, isErrorDialog:$isErrorDialogShown")
236         if (!isErrorDialogShown) {
237             startEnrollment()
238         }
239 
240         updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
241         progressViewModel.helpMessageLiveData.value.let {
242             if (it != null) {
243                 onEnrollmentHelp(it)
244             } else {
245                 updateTitleAndDescription()
246             }
247         }
248     }
249 
250     private fun reattachUdfpsEnrollView() {
251         enrollingView!!.let {
252             val newUdfpsView = LayoutInflater.from(requireActivity()).inflate(
253                 R.layout.udfps_enroll_enrolling_v2_udfps_view,
254                 null
255             )
256             val index = it.indexOfChild(udfpsEnrollView)
257             val lp = udfpsEnrollView.layoutParams
258 
259             it.removeView(udfpsEnrollView)
260             it.addView(newUdfpsView, index, lp)
261             udfpsEnrollView.setSensorProperties(
262                 enrollingViewModel.firstFingerprintSensorPropertiesInternal
263             )
264         }
265 
266         // Clear lottie status
267         haveShownTipLottie = false
268         haveShownLeftEdgeLottie = false
269         haveShownRightEdgeLottie = false
270         haveShownCenterLottie = false
271         haveShownGuideLottie = false
272         illustrationLottie?.let {
273             it.contentDescription = ""
274             it.visibility = View.GONE
275         }
276     }
277 
278     override fun onResume() {
279         super.onResume()
280         rotationViewModel.liveData.observe(this, rotationObserver)
281     }
282 
283     override fun onPause() {
284         rotationViewModel.liveData.removeObserver(rotationObserver)
285         super.onPause()
286     }
287 
288     override fun onStop() {
289         removeEnrollmentObservers()
290         val isEnrolling = progressViewModel.isEnrolling
291         val isConfigChange = requireActivity().isChangingConfigurations
292         Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
293         if (isEnrolling && !isConfigChange) {
294             cancelEnrollment(false)
295         }
296         super.onStop()
297     }
298 
299     private fun removeEnrollmentObservers() {
300         progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
301         progressViewModel.progressLiveData.removeObserver(progressObserver)
302         progressViewModel.helpMessageLiveData.removeObserver(helpMessageObserver)
303         progressViewModel.acquireLiveData.removeObserver(acquireObserver)
304         progressViewModel.pointerDownLiveData.removeObserver(pointerDownObserver)
305         progressViewModel.pointerUpLiveData.removeObserver(pointerUpObserver)
306     }
307 
308     private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
309         if (!progressViewModel.isEnrolling) {
310             Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
311             return
312         }
313         removeEnrollmentObservers()
314         if (waitForLastCancelErrMsg) {
315             progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
316         } else {
317             enrollingCancelSignal = null
318         }
319         val cancelResult: Boolean = progressViewModel.cancelEnrollment()
320         if (!cancelResult) {
321             Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
322         }
323     }
324 
325     private fun startEnrollment() {
326         enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_ENROLL)
327         if (enrollingCancelSignal == null) {
328             Log.e(TAG, "startEnrollment(), failed")
329         } else {
330             Log.d(TAG, "startEnrollment(), success")
331         }
332         progressViewModel.progressLiveData.observe(this, progressObserver)
333         progressViewModel.helpMessageLiveData.observe(this, helpMessageObserver)
334         progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
335         progressViewModel.acquireLiveData.observe(this, acquireObserver)
336         progressViewModel.pointerDownLiveData.observe(this, pointerDownObserver)
337         progressViewModel.pointerUpLiveData.observe(this, pointerUpObserver)
338     }
339 
340     private fun updateProgress(animate: Boolean, enrollmentProgress: EnrollmentProgress) {
341         if (!progressViewModel.isEnrolling) {
342             Log.d(TAG, "Enrollment not started yet")
343             return
344         }
345 
346         val progress = getProgress(enrollmentProgress)
347         Log.d(TAG, "updateProgress($animate, $enrollmentProgress), progress:$progress")
348 
349         if (enrollmentProgress.steps != -1) {
350             udfpsEnrollView.onEnrollmentProgress(
351                 enrollmentProgress.remaining,
352                 enrollmentProgress.steps
353             )
354         }
355 
356         if (progress >= PROGRESS_BAR_MAX) {
357             if (animate) {
358                 // Wait animations to finish, then proceed to next page
359                 activity!!.mainThreadHandler.postDelayed(delayedFinishRunnable, 400L)
360             } else {
361                 delayedFinishRunnable.run()
362             }
363         }
364     }
365 
366     private fun getProgress(progress: EnrollmentProgress): Int {
367         if (progress.steps == -1) {
368             return 0
369         }
370         val displayProgress = 0.coerceAtLeast(progress.steps + 1 - progress.remaining)
371         return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
372     }
373 
374     private fun updateTitleAndDescription() {
375         Log.d(TAG, "updateTitleAndDescription($currentStage)")
376         when (currentStage) {
377             STAGE_CENTER -> {
378                 titleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title)
379                 if (isAccessibilityEnabled || illustrationLottie == null) {
380                     subTitleText.setText(R.string.security_settings_udfps_enroll_start_message)
381                 } else if (!haveShownCenterLottie) {
382                     haveShownCenterLottie = true
383                     // Note: Update string reference when differentiate in between udfps & sfps
384                     illustrationLottie!!.contentDescription = getString(R.string.security_settings_sfps_enroll_finger_center_title)
385                     configureEnrollmentStage(R.raw.udfps_center_hint_lottie)
386                 }
387             }
388 
389             STAGE_GUIDED -> {
390                 titleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title)
391                 if (isAccessibilityEnabled || illustrationLottie == null) {
392                     subTitleText.setText(
393                         R.string.security_settings_udfps_enroll_repeat_a11y_message
394                     )
395                 } else if (!haveShownGuideLottie) {
396                     haveShownGuideLottie = true
397                     illustrationLottie!!.contentDescription =
398                         getString(R.string.security_settings_fingerprint_enroll_repeat_message)
399                     // TODO(b/228100413) Could customize guided lottie animation
400                     configureEnrollmentStage(R.raw.udfps_center_hint_lottie)
401                 }
402             }
403 
404             STAGE_FINGERTIP -> {
405                 titleText.setText(R.string.security_settings_udfps_enroll_fingertip_title)
406                 if (!haveShownTipLottie && illustrationLottie != null) {
407                     haveShownTipLottie = true
408                     illustrationLottie!!.contentDescription =
409                         getString(R.string.security_settings_udfps_tip_fingerprint_help)
410                     configureEnrollmentStage(R.raw.udfps_tip_hint_lottie)
411                 }
412             }
413 
414             STAGE_LEFT_EDGE -> {
415                 titleText.setText(R.string.security_settings_udfps_enroll_left_edge_title)
416                 if (!haveShownLeftEdgeLottie && illustrationLottie != null) {
417                     haveShownLeftEdgeLottie = true
418                     illustrationLottie!!.contentDescription =
419                         getString(R.string.security_settings_udfps_side_fingerprint_help)
420                     configureEnrollmentStage(R.raw.udfps_left_edge_hint_lottie)
421                 } else if (illustrationLottie == null) {
422                     if (isStageHalfCompleted) {
423                         subTitleText.setText(
424                             R.string.security_settings_fingerprint_enroll_repeat_message
425                         )
426                     } else {
427                         subTitleText.setText(R.string.security_settings_udfps_enroll_edge_message)
428                     }
429                 }
430             }
431 
432             STAGE_RIGHT_EDGE -> {
433                 titleText.setText(R.string.security_settings_udfps_enroll_right_edge_title)
434                 if (!haveShownRightEdgeLottie && illustrationLottie != null) {
435                     haveShownRightEdgeLottie = true
436                     illustrationLottie!!.contentDescription =
437                         getString(R.string.security_settings_udfps_side_fingerprint_help)
438                     configureEnrollmentStage(R.raw.udfps_right_edge_hint_lottie)
439                 } else if (illustrationLottie == null) {
440                     if (isStageHalfCompleted) {
441                         subTitleText.setText(
442                             R.string.security_settings_fingerprint_enroll_repeat_message
443                         )
444                     } else {
445                         subTitleText.setText(R.string.security_settings_udfps_enroll_edge_message)
446                     }
447                 }
448             }
449 
450             STAGE_UNKNOWN -> {
451                 titleText.setText(R.string.security_settings_fingerprint_enroll_udfps_title)
452                 subTitleText.setText(R.string.security_settings_udfps_enroll_start_message)
453                 val description: CharSequence = getString(
454                     R.string.security_settings_udfps_enroll_a11y
455                 )
456                 requireActivity().title = description
457             }
458 
459             else -> {
460                 titleText.setText(R.string.security_settings_fingerprint_enroll_udfps_title)
461                 subTitleText.setText(R.string.security_settings_udfps_enroll_start_message)
462                 val description: CharSequence = getString(
463                     R.string.security_settings_udfps_enroll_a11y
464                 )
465                 requireActivity().title = description
466             }
467         }
468     }
469 
470     private fun updateIllustrationLottie(@Surface.Rotation rotation: Int) {
471         if (rotation == ROTATION_90 || rotation == ROTATION_270) {
472             illustrationLottie = null
473         } else if (shouldShowLottie) {
474             illustrationLottie =
475                 enrollingView!!.findViewById(R.id.illustration_lottie)
476         }
477     }
478 
479     private val currentStage: Int
480         get() {
481             val progress = progressViewModel.progressLiveData.value!!
482             if (progress.steps == -1) {
483                 return STAGE_UNKNOWN
484             }
485             val progressSteps: Int = progress.steps - progress.remaining
486             return if (progressSteps < getStageThresholdSteps(0)) {
487                 STAGE_CENTER
488             } else if (progressSteps < getStageThresholdSteps(1)) {
489                 STAGE_GUIDED
490             } else if (progressSteps < getStageThresholdSteps(2)) {
491                 STAGE_FINGERTIP
492             } else if (progressSteps < getStageThresholdSteps(3)) {
493                 STAGE_LEFT_EDGE
494             } else {
495                 STAGE_RIGHT_EDGE
496             }
497         }
498 
499     private val isStageHalfCompleted: Boolean
500         get() {
501             val progress: EnrollmentProgress = progressViewModel.progressLiveData.value!!
502             if (progress.steps == -1) {
503                 return false
504             }
505             val progressSteps: Int = progress.steps - progress.remaining
506             var prevThresholdSteps = 0
507             for (i in 0 until enrollingViewModel.getEnrollStageCount()) {
508                 val thresholdSteps = getStageThresholdSteps(i)
509                 if (progressSteps in prevThresholdSteps until thresholdSteps) {
510                     val adjustedProgress = progressSteps - prevThresholdSteps
511                     val adjustedThreshold = thresholdSteps - prevThresholdSteps
512                     return adjustedProgress >= adjustedThreshold / 2
513                 }
514                 prevThresholdSteps = thresholdSteps
515             }
516 
517             // After last enrollment step.
518             return true
519         }
520 
521     private fun getStageThresholdSteps(index: Int): Int {
522         val progress: EnrollmentProgress = progressViewModel.progressLiveData.value!!
523         if (progress.steps == -1) {
524             Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet")
525             return 1
526         }
527         return (progress.steps * enrollingViewModel.getEnrollStageThreshold(index)).roundToInt()
528     }
529 
530     private fun configureEnrollmentStage(@RawRes lottie: Int) {
531         subTitleText.text = ""
532         LottieCompositionFactory.fromRawRes(activity, lottie)
533             .addListener { c: LottieComposition ->
534                 illustrationLottie?.let {
535                     it.setComposition(c)
536                     it.visibility = View.VISIBLE
537                     it.playAnimation()
538                 }
539             }
540     }
541 
542     private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
543         updateProgress(true /* animate */, progress)
544         updateTitleAndDescription()
545         if (isAccessibilityEnabled) {
546             val steps: Int = progress.steps
547             val remaining: Int = progress.remaining
548             val percent = ((steps - remaining).toFloat() / steps.toFloat() * 100).toInt()
549             val announcement: CharSequence = activity!!.getString(
550                 R.string.security_settings_udfps_enroll_progress_a11y_message, percent
551             )
552             enrollingViewModel.sendAccessibilityEvent(announcement)
553         }
554     }
555 
556     private fun onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
557         Log.d(TAG, "onEnrollmentHelp($helpMessage)")
558         val helpStr: CharSequence = helpMessage.str
559         if (helpStr.isNotEmpty()) {
560             showError(helpStr)
561             udfpsEnrollView.onEnrollmentHelp()
562         }
563     }
564 
565     private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
566         cancelEnrollment(true)
567         lifecycleScope.launch {
568             Log.d(TAG, "newDialog $errorMessage")
569             errorDialogViewModel.newDialog(errorMessage.msgId)
570         }
571     }
572 
573     private fun onEnrollmentCanceled(canceledSignal: Any) {
574         Log.d(
575             TAG,
576             "onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
577         )
578         if (enrollingCancelSignal === canceledSignal) {
579             progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
580             progressViewModel.clearProgressLiveData()
581             if (enrollingViewModel.onBackPressed) {
582                 enrollingViewModel.onCancelledDueToOnBackPressed()
583             } else if (enrollingViewModel.onSkipPressed) {
584                 enrollingViewModel.onCancelledDueToOnSkipPressed()
585             }
586         }
587     }
588 
589     private fun onAcquired(isAcquiredGood: Boolean) {
590         udfpsEnrollView.onAcquired(isAcquiredGood)
591     }
592 
593     private fun onPointerDown(sensorId: Int) {
594         udfpsEnrollView.onPointerDown(sensorId)
595     }
596 
597     private fun onPointerUp(sensorId: Int) {
598         udfpsEnrollView.onPointerUp(sensorId)
599     }
600 
601     private fun showError(error: CharSequence) {
602         titleText.text = error
603         titleText.contentDescription = error
604         subTitleText.contentDescription = ""
605     }
606 
607     private fun onRotationChanged(newRotation: Int) {
608         if ((newRotation + 2) % 4 == rotation) {
609             rotation = newRotation
610             requireContext().configLayout(newRotation, titleText, subTitleText, icon, skipBtn)
611         }
612     }
613 
614     companion object {
615         private val TAG = "FingerprintEnrollEnrollingUdfpsFragment"
616         private const val PROGRESS_BAR_MAX = 10000
617         private const val STAGE_UNKNOWN = -1
618         private const val STAGE_CENTER = 0
619         private const val STAGE_GUIDED = 1
620         private const val STAGE_FINGERTIP = 2
621         private const val STAGE_LEFT_EDGE = 3
622         private const val STAGE_RIGHT_EDGE = 4
623     }
624 }
625 
626 
FragmentActivitynull627 fun FragmentActivity.bindFingerprintEnrollEnrollingUdfpsView(
628     view: RelativeLayout,
629     sensorProperties: FingerprintSensorPropertiesInternal,
630     @Surface.Rotation rotation: Int,
631     onSkipClickListener: View.OnClickListener
632 ) {
633     view.findViewById<UdfpsEnrollView>(R.id.udfps_animation_view)!!.setSensorProperties(
634         sensorProperties
635     )
636 
637     val titleText = view.findViewById<TextView>(R.id.suc_layout_title)!!
638     val subTitleText = view.findViewById<TextView>(R.id.sud_layout_subtitle)!!
639     val icon = view.findViewById<ImageView>(R.id.sud_layout_icon)!!
640     val skipBtn = view.findViewById<Button>(R.id.skip_btn)!!.also {
641         it.setOnClickListener(onSkipClickListener)
642     }
643     configLayout(rotation, titleText, subTitleText, icon, skipBtn)
644 }
645 
Contextnull646 private fun Context.configLayout(
647     @Surface.Rotation newRotation: Int,
648     titleText: TextView,
649     subTitleText: TextView,
650     icon: ImageView,
651     skipBtn: Button
652 ) {
653     if (newRotation == ROTATION_270) {
654         val iconLP = RelativeLayout.LayoutParams(-2, -2)
655         iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
656         iconLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view)
657         iconLP.topMargin = convertDpToPixel(76.64f)
658         iconLP.leftMargin = convertDpToPixel(151.54f)
659         icon.layoutParams = iconLP
660         val titleLP = RelativeLayout.LayoutParams(-1, -2)
661         titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
662         titleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view)
663         titleLP.topMargin = convertDpToPixel(138f)
664         titleLP.leftMargin = convertDpToPixel(144f)
665         titleText.layoutParams = titleLP
666         val subtitleLP = RelativeLayout.LayoutParams(-1, -2)
667         subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
668         subtitleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view)
669         subtitleLP.topMargin = convertDpToPixel(198f)
670         subtitleLP.leftMargin = convertDpToPixel(144f)
671         subTitleText.layoutParams = subtitleLP
672     } else if (newRotation == ROTATION_90) {
673         val metrics = resources.displayMetrics
674         val iconLP = RelativeLayout.LayoutParams(-2, -2)
675         iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
676         iconLP.addRule(RelativeLayout.ALIGN_PARENT_START)
677         iconLP.topMargin = convertDpToPixel(76.64f)
678         iconLP.leftMargin = convertDpToPixel(71.99f)
679         icon.layoutParams = iconLP
680         val titleLP = RelativeLayout.LayoutParams(
681             metrics.widthPixels / 2, -2
682         )
683         titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
684         titleLP.addRule(RelativeLayout.ALIGN_PARENT_START, R.id.udfps_animation_view)
685         titleLP.topMargin = convertDpToPixel(138f)
686         titleLP.leftMargin = convertDpToPixel(66f)
687         titleText.layoutParams = titleLP
688         val subtitleLP = RelativeLayout.LayoutParams(
689             metrics.widthPixels / 2, -2
690         )
691         subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
692         subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_START)
693         subtitleLP.topMargin = convertDpToPixel(198f)
694         subtitleLP.leftMargin = convertDpToPixel(66f)
695         subTitleText.layoutParams = subtitleLP
696     }
697     if (newRotation == ROTATION_90 || newRotation == ROTATION_270) {
698         val skipBtnLP = skipBtn.layoutParams as RelativeLayout.LayoutParams
699         skipBtnLP.topMargin = convertDpToPixel(26f)
700         skipBtnLP.leftMargin = convertDpToPixel(54f)
701         skipBtn.requestLayout()
702     }
703 }
704 
Contextnull705 fun Context.convertDpToPixel(dp: Float): Int {
706     return (dp * resources.displayMetrics.density).toInt()
707 }
708