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.app.admin.DevicePolicyManager
19 import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED
20 import android.content.Context
21 import android.graphics.PorterDuff
22 import android.graphics.PorterDuffColorFilter
23 import android.os.Bundle
24 import android.text.Html
25 import android.text.method.LinkMovementMethod
26 import android.util.Log
27 import android.view.LayoutInflater
28 import android.view.View
29 import android.view.ViewGroup
30 import android.widget.ImageView
31 import android.widget.ScrollView
32 import android.widget.TextView
33 import androidx.annotation.StringRes
34 import androidx.fragment.app.Fragment
35 import androidx.fragment.app.FragmentActivity
36 import androidx.lifecycle.Lifecycle
37 import androidx.lifecycle.ViewModelProvider
38 import androidx.lifecycle.lifecycleScope
39 import androidx.lifecycle.repeatOnLifecycle
40 import com.android.settings.R
41 import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus
42 import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
43 import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK
44 import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN
45 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel
46 import com.google.android.setupcompat.template.FooterBarMixin
47 import com.google.android.setupcompat.template.FooterButton
48 import com.google.android.setupdesign.GlifLayout
49 import com.google.android.setupdesign.template.RequireScrollMixin
50 import com.google.android.setupdesign.util.DeviceHelper
51 import com.google.android.setupdesign.util.DynamicColorPalette
52 import com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT
53 import java.util.function.Supplier
54 import kotlinx.coroutines.flow.first
55 import kotlinx.coroutines.launch
56
57 /**
58 * Fingerprint intro onboarding page fragment implementation
59 */
60 class FingerprintEnrollIntroFragment : Fragment() {
61
62 private val viewModelProvider: ViewModelProvider
63 get() = ViewModelProvider(requireActivity())
64
65 private var _viewModel: FingerprintEnrollIntroViewModel? = null
66 private val viewModel: FingerprintEnrollIntroViewModel
67 get() = _viewModel!!
68
69 private var introView: GlifLayout? = null
70
71 private var primaryFooterButton: FooterButton? = null
72
73 private var secondaryFooterButton: FooterButton? = null
74
75 private val onNextClickListener =
76 View.OnClickListener { _: View? ->
77 activity?.lifecycleScope?.let {
78 viewModel.onNextButtonClick(it)
79 }
80 }
81
82 private val onSkipOrCancelClickListener =
83 View.OnClickListener { _: View? ->
84 activity?.lifecycleScope?.let {
85 viewModel.onSkipOrCancelButtonClick(it)
86 }
87 }
88
89 override fun onCreateView(
90 inflater: LayoutInflater,
91 container: ViewGroup?,
92 savedInstanceState: Bundle?
93 ): View {
94 introView = inflater.inflate(
95 R.layout.fingerprint_enroll_introduction,
96 container,
97 false
98 ) as GlifLayout
99 return introView!!
100 }
101
102 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
103 super.onViewCreated(view, savedInstanceState)
104 requireActivity().bindFingerprintEnrollIntroView(
105 view = introView!!,
106 canAssumeUdfps = viewModel.canAssumeUdfps,
107 isBiometricUnlockDisabledByAdmin = viewModel.isBiometricUnlockDisabledByAdmin,
108 isParentalConsentRequired = viewModel.isParentalConsentRequired,
109 descriptionDisabledByAdminSupplier = { getDescriptionDisabledByAdmin(view.context) }
110 )
111 }
112
113 override fun onStart() {
114 val context: Context = requireContext()
115 val footerBarMixin: FooterBarMixin = footerBarMixin
116 viewModel.updateEnrollableStatus(lifecycleScope)
117 initPrimaryFooterButton(context, footerBarMixin)
118 initSecondaryFooterButton(context, footerBarMixin)
119 collectPageStatusFlowIfNeed()
120 super.onStart()
121 }
122
123 private fun initPrimaryFooterButton(
124 context: Context,
125 footerBarMixin: FooterBarMixin
126 ) {
127 if (footerBarMixin.primaryButton != null) {
128 return
129 }
130 primaryFooterButton = FooterButton.Builder(context)
131 .setText(R.string.security_settings_fingerprint_enroll_introduction_agree)
132 .setButtonType(FooterButton.ButtonType.OPT_IN)
133 .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
134 .build()
135 .also {
136 it.setOnClickListener(onNextClickListener)
137 footerBarMixin.primaryButton = it
138 }
139 }
140
141 private fun initSecondaryFooterButton(
142 context: Context,
143 footerBarMixin: FooterBarMixin
144 ) {
145 if (footerBarMixin.secondaryButton != null) {
146 return
147 }
148 secondaryFooterButton = FooterButton.Builder(context)
149 .setText(
150 if (viewModel.request.isAfterSuwOrSuwSuggestedAction)
151 R.string.security_settings_fingerprint_enroll_introduction_cancel
152 else
153 R.string.security_settings_fingerprint_enroll_introduction_no_thanks
154 )
155 .setButtonType(FooterButton.ButtonType.NEXT)
156 .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
157 .build()
158 .also {
159 it.setOnClickListener(onSkipOrCancelClickListener)
160 footerBarMixin.setSecondaryButton(it, true /* usePrimaryStyle */)
161 }
162 }
163
164 private fun collectPageStatusFlowIfNeed() {
165 lifecycleScope.launch {
166 val status = viewModel.pageStatusFlow.first()
167 Log.d(TAG, "collectPageStatusFlowIfNeed status:$status")
168 if (status.hasScrollToBottom()
169 || status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
170 ) {
171 // Update once and do not requireScrollWithButton() again when page has
172 // scrolled to bottom or User has enrolled at least a fingerprint, because if
173 // we requireScrollWithButton() again, primary button will become "More" after
174 // scrolling.
175 updateFooterButtons(status)
176 } else {
177 introView!!.getMixin(RequireScrollMixin::class.java).let {
178 it.requireScrollWithButton(
179 requireActivity(),
180 primaryFooterButton!!,
181 moreButtonTextRes,
182 onNextClickListener
183 )
184 it.setOnRequireScrollStateChangedListener { scrollNeeded: Boolean ->
185 viewModel.setHasScrolledToBottom(!scrollNeeded, lifecycleScope)
186 }
187 }
188 repeatOnLifecycle(Lifecycle.State.STARTED) {
189 viewModel.pageStatusFlow.collect(
190 this@FingerprintEnrollIntroFragment::updateFooterButtons
191 )
192 }
193 }
194 }
195 }
196
197 override fun onAttach(context: Context) {
198 _viewModel = viewModelProvider[FingerprintEnrollIntroViewModel::class.java]
199 super.onAttach(context)
200 }
201
202 private val footerBarMixin: FooterBarMixin
203 get() = introView!!.getMixin(FooterBarMixin::class.java)
204
205 private fun getDescriptionDisabledByAdmin(context: Context): String? {
206 val defaultStrId: Int =
207 R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled
208 val devicePolicyManager: DevicePolicyManager =
209 checkNotNull(requireActivity().getSystemService(DevicePolicyManager::class.java))
210
211 return devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED) {
212 context.getString(defaultStrId)
213 }
214 }
215
216 private fun updateFooterButtons(status: FingerprintEnrollIntroStatus) {
217 if (DEBUG) {
218 Log.d(TAG, "updateFooterButtons($status)")
219 }
220 primaryFooterButton!!.setText(
221 context,
222 if (status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
223 R.string.done
224 else if (status.hasScrollToBottom())
225 R.string.security_settings_fingerprint_enroll_introduction_agree
226 else
227 moreButtonTextRes
228 )
229 secondaryFooterButton!!.visibility =
230 if (status.hasScrollToBottom()
231 && status.enrollableStatus !== FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
232 )
233 View.VISIBLE
234 else
235 View.INVISIBLE
236
237 view!!.requireViewById<TextView>(R.id.error_text).let {
238 when (status.enrollableStatus) {
239 FINGERPRINT_ENROLLABLE_OK -> {
240 it.text = null
241 it.visibility = View.GONE
242 }
243
244 FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX -> {
245 it.setText(R.string.fingerprint_intro_error_max)
246 it.visibility = View.VISIBLE
247 }
248
249 FINGERPRINT_ENROLLABLE_UNKNOWN -> {}
250 }
251 }
252 }
253
254 @get:StringRes
255 private val moreButtonTextRes: Int
256 get() = R.string.security_settings_face_enroll_introduction_more
257
258 companion object {
259 private const val TAG = "FingerprintEnrollIntroFragment"
260 private const val DEBUG = false
261 }
262 }
263
FragmentActivitynull264 fun FragmentActivity.bindFingerprintEnrollIntroView(
265 view: GlifLayout,
266 canAssumeUdfps: Boolean,
267 isBiometricUnlockDisabledByAdmin: Boolean,
268 isParentalConsentRequired: Boolean,
269 descriptionDisabledByAdminSupplier: Supplier<String?>
270 ) {
271 val context = view.context
272
273 val iconFingerprint = view.findViewById<ImageView>(R.id.icon_fingerprint)!!
274 val iconDeviceLocked = view.findViewById<ImageView>(R.id.icon_device_locked)!!
275 val iconTrashCan = view.findViewById<ImageView>(R.id.icon_trash_can)!!
276 val iconInfo = view.findViewById<ImageView>(R.id.icon_info)!!
277 val iconShield = view.findViewById<ImageView>(R.id.icon_shield)!!
278 val iconLink = view.findViewById<ImageView>(R.id.icon_link)!!
279 val footerMessage6 = view.findViewById<TextView>(R.id.footer_message_6)!!
280
281 PorterDuffColorFilter(
282 DynamicColorPalette.getColor(context, ACCENT),
283 PorterDuff.Mode.SRC_IN
284 ).let { colorFilter ->
285 iconFingerprint.drawable.colorFilter = colorFilter
286 iconDeviceLocked.drawable.colorFilter = colorFilter
287 iconTrashCan.drawable.colorFilter = colorFilter
288 iconInfo.drawable.colorFilter = colorFilter
289 iconShield.drawable.colorFilter = colorFilter
290 iconLink.drawable.colorFilter = colorFilter
291 }
292
293 view.findViewById<TextView>(R.id.footer_learn_more)!!.let { learnMore ->
294 learnMore.movementMethod = LinkMovementMethod.getInstance()
295 val footerLinkStr: String = context.getString(
296 R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more,
297 Html.FROM_HTML_MODE_LEGACY
298 )
299 learnMore.text = Html.fromHtml(footerLinkStr)
300 }
301
302 if (canAssumeUdfps) {
303 footerMessage6.visibility = View.VISIBLE
304 iconShield.visibility = View.VISIBLE
305 } else {
306 footerMessage6.visibility = View.GONE
307 iconShield.visibility = View.GONE
308 }
309 val glifLayoutHelper = GlifLayoutHelper(this, view)
310 if (isBiometricUnlockDisabledByAdmin && !isParentalConsentRequired) {
311 glifLayoutHelper.setHeaderText(
312 R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled
313 )
314 glifLayoutHelper.setDescriptionText(descriptionDisabledByAdminSupplier.get())
315 } else {
316 glifLayoutHelper.setHeaderText(
317 R.string.security_settings_fingerprint_enroll_introduction_title
318 )
319 glifLayoutHelper.setDescriptionText(
320 getString(
321 R.string.security_settings_fingerprint_enroll_introduction_v3_message,
322 DeviceHelper.getDeviceName(context)
323 )
324 )
325 }
326
327 view.findViewById<ScrollView>(com.google.android.setupdesign.R.id.sud_scroll_view)
328 ?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
329 }
330