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