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