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