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.annotation.RawRes
27 import androidx.fragment.app.Fragment
28 import androidx.fragment.app.FragmentActivity
29 import androidx.lifecycle.Lifecycle
30 import androidx.lifecycle.LiveData
31 import androidx.lifecycle.Observer
32 import androidx.lifecycle.ViewModelProvider
33 import androidx.lifecycle.lifecycleScope
34 import androidx.lifecycle.repeatOnLifecycle
35 import com.airbnb.lottie.LottieAnimationView
36 import com.android.settings.R
37 import com.android.settings.biometrics2.ui.model.EnrollmentProgress
38 import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
39 import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel
40 import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel
41 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
42 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel
43 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
44 import com.android.settingslib.widget.LottieColorUtils
45 import com.google.android.setupcompat.template.FooterBarMixin
46 import com.google.android.setupcompat.template.FooterButton
47 import com.google.android.setupdesign.GlifLayout
48 import kotlinx.coroutines.launch
49
50 /**
51 * Fragment explaining the side fingerprint sensor location for fingerprint enrollment.
52 * It interacts with ProgressViewModel, FoldCallback (for different lottie), and
53 * LottieAnimationView.
54 * <pre>
55 * | Has | UDFPS | SFPS | Other (Rear FPS) |
56 * |---------------------|-------|------|------------------|
57 * | Primary button | Yes | No | No |
58 * | Illustration Lottie | Yes | Yes | No |
59 * | Animation | No | No | Depend on layout |
60 * | Progress ViewModel | No | Yes | Yes |
61 * | Orientation detect | No | Yes | No |
62 * | Foldable detect | No | Yes | No |
63 * </pre>
64 */
65 class FingerprintEnrollFindSfpsFragment : Fragment() {
66
67 private var _viewModel: FingerprintEnrollFindSensorViewModel? = null
68 private val viewModel: FingerprintEnrollFindSensorViewModel
69 get() = _viewModel!!
70
71 private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
72 private val progressViewModel: FingerprintEnrollProgressViewModel
73 get() = _progressViewModel!!
74
75 private var _rotationViewModel: DeviceRotationViewModel? = null
76 private val rotationViewModel: DeviceRotationViewModel
77 get() = _rotationViewModel!!
78
79 private var _foldedViewModel: DeviceFoldedViewModel? = null
80 private val foldedViewModel: DeviceFoldedViewModel
81 get() = _foldedViewModel!!
82
83 private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
84 private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
85 get() = _errorDialogViewModel!!
86
87 private var findSfpsView: GlifLayout? = null
88
89 private val onSkipClickListener =
90 View.OnClickListener { _: View? -> viewModel.onSkipButtonClick() }
91
92 private val illustrationLottie: LottieAnimationView
93 get() = findSfpsView!!.findViewById(R.id.illustration_lottie)!!
94
95 private var enrollingCancelSignal: Any? = null
96
97 @Surface.Rotation
98 private var animationRotation = -1
99
100 private val rotationObserver = Observer { rotation: Int? ->
101 rotation?.let { onRotationChanged(it) }
102 }
103
104 private val progressObserver = Observer { progress: EnrollmentProgress? ->
105 if (progress != null && !progress.isInitialStep) {
106 cancelEnrollment(true)
107 }
108 }
109
110 private val errorMessageObserver = Observer{ errorMessage: EnrollmentStatusMessage? ->
111 Log.d(TAG, "errorMessageObserver($errorMessage)")
112 errorMessage?.let { onEnrollmentError(it) }
113 }
114
115 private val canceledSignalObserver = Observer { canceledSignal: Any? ->
116 canceledSignal?.let { onEnrollmentCanceled(it) }
117 }
118
119 override fun onCreateView(
120 inflater: LayoutInflater, container: ViewGroup?,
121 savedInstanceState: Bundle?
122 ): View = (inflater.inflate(
123 R.layout.sfps_enroll_find_sensor_layout,
124 container,
125 false
126 ) as GlifLayout).also {
127 findSfpsView = it
128 }
129
130 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
131 super.onViewCreated(view, savedInstanceState)
132 requireActivity().bindFingerprintEnrollFindSfpsView(
133 view = findSfpsView!!,
134 onSkipClickListener = onSkipClickListener
135 )
136
137 lifecycleScope.launch {
138 repeatOnLifecycle(Lifecycle.State.STARTED) {
139 errorDialogViewModel.triggerRetryFlow.collect { startEnrollment() }
140 }
141 }
142 }
143
144 override fun onStart() {
145 super.onStart()
146 val isErrorDialogShown = errorDialogViewModel.isDialogShown
147 Log.d(TAG, "onStart(), isEnrolling:${progressViewModel.isEnrolling}"
148 + ", isErrorDialog:$isErrorDialogShown")
149 if (!isErrorDialogShown) {
150 startEnrollment()
151 }
152 }
153
154 override fun onResume() {
155 super.onResume()
156 val rotationLiveData: LiveData<Int> = rotationViewModel.liveData
157 playLottieAnimation(rotationLiveData.value!!)
158 rotationLiveData.observe(this, rotationObserver)
159 }
160
161 override fun onPause() {
162 rotationViewModel.liveData.removeObserver(rotationObserver)
163 super.onPause()
164 }
165
166 override fun onStop() {
167 super.onStop()
168 val isEnrolling = progressViewModel.isEnrolling
169 val isConfigChange = requireActivity().isChangingConfigurations
170 Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
171 if (isEnrolling && !isConfigChange) {
172 cancelEnrollment(false)
173 }
174 }
175
176 private fun removeEnrollmentObservers() {
177 progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
178 progressViewModel.progressLiveData.removeObserver(progressObserver)
179 }
180
181 private fun startEnrollment() {
182 enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_FIND_SENSOR)
183 if (enrollingCancelSignal == null) {
184 Log.e(TAG, "startEnrollment(), failed to start enrollment")
185 } else {
186 Log.d(TAG, "startEnrollment(), success")
187 }
188 progressViewModel.progressLiveData.observe(this, progressObserver)
189 progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
190 }
191
192 private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
193 if (!progressViewModel.isEnrolling) {
194 Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
195 return
196 }
197 removeEnrollmentObservers()
198 if (waitForLastCancelErrMsg) {
199 progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
200 } else {
201 enrollingCancelSignal = null
202 }
203 val cancelResult: Boolean = progressViewModel.cancelEnrollment()
204 if (!cancelResult) {
205 Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
206 }
207 }
208
209 private fun onRotationChanged(@Surface.Rotation newRotation: Int) {
210 if (DEBUG) {
211 Log.d(TAG, "onRotationChanged() from $animationRotation to $newRotation")
212 }
213 if ((newRotation + 2) % 4 == animationRotation) {
214 // Fragment not changed, we just need to play correct rotation animation
215 playLottieAnimation(newRotation)
216 }
217 }
218
219 private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
220 progressViewModel.cancelEnrollment()
221 lifecycleScope.launch {
222 Log.d(TAG, "newDialogFlow as $errorMessage")
223 errorDialogViewModel.newDialog(errorMessage.msgId)
224 }
225 }
226
227 private fun onEnrollmentCanceled(canceledSignal: Any) {
228 Log.d(
229 TAG,
230 "onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
231 )
232 if (enrollingCancelSignal === canceledSignal) {
233 val progress: EnrollmentProgress? = progressViewModel.progressLiveData.value
234 progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
235 progressViewModel.clearProgressLiveData()
236 if (progress != null && !progress.isInitialStep) {
237 viewModel.onStartButtonClick()
238 }
239 }
240 }
241
242 private fun playLottieAnimation(@Surface.Rotation rotation: Int) {
243 @RawRes val animationRawRes = getSfpsLottieAnimationRawRes(rotation)
244 Log.d(
245 TAG,
246 "play lottie animation $animationRawRes, previous rotation:$animationRotation"
247 + ", new rotation:" + rotation
248 )
249 animationRotation = rotation
250 illustrationLottie.setAnimation(animationRawRes)
251 LottieColorUtils.applyDynamicColors(activity, illustrationLottie)
252 illustrationLottie.visibility = View.VISIBLE
253 illustrationLottie.playAnimation()
254 }
255
256 @RawRes
257 private fun getSfpsLottieAnimationRawRes(@Surface.Rotation rotation: Int): Int {
258 val isFolded = java.lang.Boolean.FALSE != foldedViewModel.liveData.value
259 return when (rotation) {
260 Surface.ROTATION_90 ->
261 if (isFolded)
262 R.raw.fingerprint_edu_lottie_folded_top_left
263 else
264 R.raw.fingerprint_edu_lottie_portrait_top_left
265 Surface.ROTATION_180 ->
266 if (isFolded)
267 R.raw.fingerprint_edu_lottie_folded_bottom_left
268 else
269 R.raw.fingerprint_edu_lottie_landscape_bottom_left
270 Surface.ROTATION_270 ->
271 if (isFolded)
272 R.raw.fingerprint_edu_lottie_folded_bottom_right
273 else
274 R.raw.fingerprint_edu_lottie_portrait_bottom_right
275 else ->
276 if (isFolded)
277 R.raw.fingerprint_edu_lottie_folded_top_right
278 else
279 R.raw.fingerprint_edu_lottie_landscape_top_right
280 }
281 }
282
283 override fun onAttach(context: Context) {
284 ViewModelProvider(requireActivity()).let { provider ->
285 _viewModel = provider[FingerprintEnrollFindSensorViewModel::class.java]
286 _progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
287 _rotationViewModel = provider[DeviceRotationViewModel::class.java]
288 _foldedViewModel = provider[DeviceFoldedViewModel::class.java]
289 _errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
290 }
291 super.onAttach(context)
292 }
293
294 companion object {
295 private const val DEBUG = false
296 private const val TAG = "FingerprintEnrollFindSfpsFragment"
297 }
298 }
299
bindFingerprintEnrollFindSfpsViewnull300 fun FragmentActivity.bindFingerprintEnrollFindSfpsView(
301 view: GlifLayout,
302 onSkipClickListener: View.OnClickListener
303 ) {
304 view.getMixin(FooterBarMixin::class.java).let {
305 it.secondaryButton = FooterButton.Builder(this)
306 .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
307 .setButtonType(FooterButton.ButtonType.SKIP)
308 .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
309 .build()
310 it.secondaryButton.setOnClickListener(onSkipClickListener)
311 }
312
313 GlifLayoutHelper(this, view).let {
314 it.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
315 it.setDescriptionText(
316 getText(R.string.security_settings_sfps_enroll_find_sensor_message)
317 )
318 }
319 }
320