1 /*
<lambda>null2 * Copyright (C) 2022 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.content.Context
19 import android.hardware.fingerprint.FingerprintManager.ENROLL_FIND_SENSOR
20 import android.os.Bundle
21 import android.util.Log
22 import android.view.LayoutInflater
23 import android.view.Surface
24 import android.view.View
25 import android.view.ViewGroup
26 import androidx.fragment.app.Fragment
27 import androidx.fragment.app.FragmentActivity
28 import androidx.lifecycle.Lifecycle
29 import androidx.lifecycle.LiveData
30 import androidx.lifecycle.Observer
31 import androidx.lifecycle.ViewModelProvider
32 import androidx.lifecycle.lifecycleScope
33 import androidx.lifecycle.repeatOnLifecycle
34 import com.android.settings.R
35 import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
36 import com.android.settings.biometrics2.ui.model.EnrollmentProgress
37 import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
38 import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel
39 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
40 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel
41 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
42 import com.google.android.setupcompat.template.FooterBarMixin
43 import com.google.android.setupcompat.template.FooterButton
44 import com.google.android.setupdesign.GlifLayout
45 import kotlinx.coroutines.launch
46
47 /**
48 * Fragment explaining the side fingerprint sensor location for fingerprint enrollment.
49 * It interacts with ProgressViewModel, and FingerprintFindSensorAnimation.
50 * <pre>
51 * | Has | UDFPS | SFPS | Other (Rear FPS) |
52 * |---------------------|-------|------|------------------|
53 * | Primary button | Yes | No | No |
54 * | Illustration Lottie | Yes | Yes | No |
55 * | Animation | No | No | Depend on layout |
56 * | Progress ViewModel | No | Yes | Yes |
57 * | Orientation detect | No | Yes | No |
58 * | Foldable detect | No | Yes | No |
59 * </pre>
60 */
61 class FingerprintEnrollFindRfpsFragment : Fragment() {
62
63 private var _viewModel: FingerprintEnrollFindSensorViewModel? = null
64 private val viewModel: FingerprintEnrollFindSensorViewModel
65 get() = _viewModel!!
66
67 private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
68 private val progressViewModel: FingerprintEnrollProgressViewModel
69 get() = _progressViewModel!!
70
71 private var _rotationViewModel: DeviceRotationViewModel? = null
72 private val rotationViewModel: DeviceRotationViewModel
73 get() = _rotationViewModel!!
74
75 private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
76 private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
77 get() = _errorDialogViewModel!!
78
79 private var findRfpsView: GlifLayout? = null
80
81 private val onSkipClickListener =
82 View.OnClickListener { _: View? -> viewModel.onSkipButtonClick() }
83
84 private var animation: FingerprintFindSensorAnimation? = null
85
86 private var enrollingCancelSignal: Any? = null
87
88 @Surface.Rotation
89 private var lastRotation = -1
90
91 private val progressObserver = Observer { progress: EnrollmentProgress? ->
92 if (progress != null && !progress.isInitialStep) {
93 cancelEnrollment(true)
94 }
95 }
96
97 private val errorMessageObserver = Observer { errorMessage: EnrollmentStatusMessage? ->
98 Log.d(TAG, "errorMessageObserver($errorMessage)")
99 errorMessage?.let { onEnrollmentError(it) }
100 }
101
102 private val canceledSignalObserver = Observer { canceledSignal: Any? ->
103 canceledSignal?.let { onEnrollmentCanceled(it) }
104 }
105
106 override fun onCreateView(
107 inflater: LayoutInflater, container: ViewGroup?,
108 savedInstanceState: Bundle?
109 ): View {
110 findRfpsView = inflater.inflate(
111 R.layout.fingerprint_enroll_find_sensor,
112 container,
113 false
114 ) as GlifLayout
115
116 val animationView = findRfpsView!!.findViewById<View>(
117 R.id.fingerprint_sensor_location_animation
118 )
119 if (animationView is FingerprintFindSensorAnimation) {
120 animation = animationView
121 }
122
123 return findRfpsView!!
124 }
125
126 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
127 super.onViewCreated(view, savedInstanceState)
128 requireActivity().bindFingerprintEnrollFindRfpsView(
129 view = findRfpsView!!,
130 onSkipClickListener = onSkipClickListener
131 )
132
133 lifecycleScope.launch {
134 repeatOnLifecycle(Lifecycle.State.STARTED) {
135 errorDialogViewModel.triggerRetryFlow.collect { retryLookingForFingerprint() }
136 }
137 }
138 }
139
140 private fun retryLookingForFingerprint() {
141 startEnrollment()
142 animation?.let {
143 Log.d(TAG, "retry, start animation")
144 it.startAnimation()
145 }
146 }
147
148 override fun onStart() {
149 super.onStart()
150 val isErrorDialogShown = errorDialogViewModel.isDialogShown
151 Log.d(TAG, "onStart(), isEnrolling:${progressViewModel.isEnrolling}"
152 + ", isErrorDialog:$isErrorDialogShown")
153 if (!isErrorDialogShown) {
154 startEnrollment()
155 }
156 }
157
158 override fun onResume() {
159 val rotationLiveData: LiveData<Int> = rotationViewModel.liveData
160 lastRotation = rotationLiveData.value!!
161 if (!errorDialogViewModel.isDialogShown) {
162 animation?.let {
163 Log.d(TAG, "onResume(), start animation")
164 it.startAnimation()
165 }
166 }
167 super.onResume()
168 }
169
170 override fun onPause() {
171 animation?.let {
172 if (DEBUG) {
173 Log.d(TAG, "onPause(), pause animation")
174 }
175 it.pauseAnimation()
176 }
177 super.onPause()
178 }
179
180 override fun onStop() {
181 super.onStop()
182 removeEnrollmentObservers()
183 val isEnrolling = progressViewModel.isEnrolling
184 val isConfigChange = requireActivity().isChangingConfigurations
185 Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
186 if (isEnrolling && !isConfigChange) {
187 cancelEnrollment(false)
188 }
189 }
190
191 private fun removeEnrollmentObservers() {
192 progressViewModel.progressLiveData.removeObserver(progressObserver)
193 progressViewModel.helpMessageLiveData.removeObserver(errorMessageObserver)
194 }
195
196 private fun startEnrollment() {
197 enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_FIND_SENSOR)
198 if (enrollingCancelSignal == null) {
199 Log.e(TAG, "startEnrollment(), failed to start enrollment")
200 } else {
201 Log.d(TAG, "startEnrollment(), success")
202 }
203 progressViewModel.progressLiveData.observe(this, progressObserver)
204 progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
205 }
206
207 private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
208 if (!progressViewModel.isEnrolling) {
209 Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
210 return
211 }
212 removeEnrollmentObservers()
213 if (waitForLastCancelErrMsg) {
214 progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
215 } else {
216 enrollingCancelSignal = null
217 }
218 val cancelResult: Boolean = progressViewModel.cancelEnrollment()
219 if (!cancelResult) {
220 Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
221 }
222 }
223
224 private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
225 cancelEnrollment(false)
226 lifecycleScope.launch {
227 Log.d(TAG, "newDialogFlow as $errorMessage")
228 errorDialogViewModel.newDialog(errorMessage.msgId)
229 }
230 }
231
232 private fun onEnrollmentCanceled(canceledSignal: Any) {
233 Log.d(
234 TAG,
235 "onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
236 )
237 if (enrollingCancelSignal === canceledSignal) {
238 val progress: EnrollmentProgress? = progressViewModel.progressLiveData.value
239 progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
240 progressViewModel.clearProgressLiveData()
241 if (progress != null && !progress.isInitialStep) {
242 viewModel.onStartButtonClick()
243 }
244 }
245 }
246
247 override fun onDestroy() {
248 animation?.let {
249 if (DEBUG) {
250 Log.d(TAG, "onDestroy(), stop animation")
251 }
252 it.stopAnimation()
253 }
254 super.onDestroy()
255 }
256
257 override fun onAttach(context: Context) {
258 ViewModelProvider(requireActivity()).let { provider ->
259 _viewModel = provider[FingerprintEnrollFindSensorViewModel::class.java]
260 _progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
261 _rotationViewModel = provider[DeviceRotationViewModel::class.java]
262 _errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
263 }
264 super.onAttach(context)
265 }
266
267 companion object {
268 private const val DEBUG = false
269 private const val TAG = "FingerprintEnrollFindRfpsFragment"
270 }
271 }
272
FragmentActivitynull273 fun FragmentActivity.bindFingerprintEnrollFindRfpsView(
274 view: GlifLayout,
275 onSkipClickListener: View.OnClickListener,
276 ) {
277 GlifLayoutHelper(this, view).let {
278 it.setHeaderText(
279 R.string.security_settings_fingerprint_enroll_find_sensor_title
280 )
281 it.setDescriptionText(
282 getText(R.string.security_settings_fingerprint_enroll_find_sensor_message)
283 )
284 }
285
286 view.getMixin(FooterBarMixin::class.java).secondaryButton =
287 FooterButton.Builder(this)
288 .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
289 .setButtonType(FooterButton.ButtonType.SKIP)
290 .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
291 .build()
292 .also {
293 it.setOnClickListener(onSkipClickListener)
294 }
295 }
296