• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.settings.biometrics.fingerprint2.ui.fragment
18 
19 import android.app.Activity
20 import android.app.admin.DevicePolicyManager
21 import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED_EXPLANATION
22 import android.app.settings.SettingsEnums
23 import android.content.Context.FINGERPRINT_SERVICE
24 import android.content.Intent
25 import android.hardware.fingerprint.FingerprintManager
26 import android.os.Bundle
27 import android.provider.Settings.Secure
28 import android.text.TextUtils
29 import android.util.FeatureFlagUtils
30 import android.util.Log
31 import android.view.View
32 import android.widget.Toast
33 import androidx.activity.result.ActivityResultLauncher
34 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
35 import androidx.lifecycle.ViewModelProvider
36 import androidx.lifecycle.lifecycleScope
37 import androidx.preference.Preference
38 import androidx.preference.PreferenceCategory
39 import com.android.internal.widget.LockPatternUtils
40 import com.android.settings.R
41 import com.android.settings.Utils.SETTINGS_PACKAGE_NAME
42 import com.android.settings.biometrics.BiometricEnrollBase
43 import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
44 import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY
45 import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
46 import com.android.settings.biometrics.GatekeeperPasswordProvider
47 import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
48 import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
49 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
50 import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder
51 import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
52 import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
53 import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
54 import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
55 import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
56 import com.android.settings.core.SettingsBaseActivity
57 import com.android.settings.core.instrumentation.InstrumentedDialogFragment
58 import com.android.settings.dashboard.DashboardFragment
59 import com.android.settings.password.ChooseLockGeneric
60 import com.android.settings.password.ChooseLockSettingsHelper
61 import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
62 import com.android.settingslib.HelpUtils
63 import com.android.settingslib.RestrictedLockUtils
64 import com.android.settingslib.RestrictedLockUtilsInternal
65 import com.android.settingslib.transition.SettingsTransitionHelper
66 import com.android.settingslib.widget.FooterPreference
67 import com.google.android.setupdesign.util.DeviceHelper
68 import kotlinx.coroutines.Dispatchers
69 import kotlinx.coroutines.launch
70 
71 private const val TAG = "FingerprintSettingsV2Fragment"
72 private const val KEY_FINGERPRINTS_ENROLLED_CATEGORY = "security_settings_fingerprints_enrolled"
73 private const val KEY_FINGERPRINT_SIDE_FPS_CATEGORY =
74   "security_settings_fingerprint_unlock_category"
75 private const val KEY_FINGERPRINT_ADD = "key_fingerprint_add"
76 private const val KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH =
77   "security_settings_require_screen_on_to_auth"
78 private const val KEY_FINGERPRINT_FOOTER = "security_settings_fingerprint_footer"
79 
80 /**
81  * A class responsible for showing FingerprintSettings. Typical activity Flows are
82  * 1. Settings > FingerprintSettings > PIN/PATTERN/PASS -> FingerprintSettings
83  * 2. FingerprintSettings -> FingerprintEnrollment fow
84  *
85  * This page typically allows for
86  * 1. Fingerprint deletion
87  * 2. Fingerprint enrollment
88  * 3. Renaming a fingerprint
89  * 4. Enabling/Disabling a feature
90  */
91 class FingerprintSettingsV2Fragment :
92   DashboardFragment(), FingerprintSettingsViewBinder.FingerprintView {
93   private lateinit var settingsViewModel: FingerprintSettingsViewModel
94   private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
95 
96   /** Result listener for ChooseLock activity flow. */
97   private val confirmDeviceResultListener =
98     registerForActivityResult(StartActivityForResult()) { result ->
99       val resultCode = result.resultCode
100       val data = result.data
101       onConfirmDevice(resultCode, data)
102     }
103 
104   /** Result listener for launching enrollments **after** a user has reached the settings page. */
105   private val launchAdditionalFingerprintListener: ActivityResultLauncher<Intent> =
106     registerForActivityResult(StartActivityForResult()) { result ->
107       lifecycleScope.launch {
108         val resultCode = result.resultCode
109         Log.d(TAG, "onEnrollAdditionalFingerprint($resultCode)")
110 
111         if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
112           navigationViewModel.onEnrollAdditionalFailure()
113         } else {
114           navigationViewModel.onEnrollSuccess()
115         }
116       }
117     }
118 
119   /** Initial listener for the first enrollment request */
120   private val launchFirstEnrollmentListener: ActivityResultLauncher<Intent> =
121     registerForActivityResult(StartActivityForResult()) { result ->
122       lifecycleScope.launch {
123         val resultCode = result.resultCode
124         val data = result.data
125 
126         Log.d(TAG, "onEnrollFirstFingerprint($resultCode, $data)")
127         if (resultCode != RESULT_FINISHED || data == null) {
128           if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
129             navigationViewModel.onEnrollFirstFailure(
130               "Received RESULT_TIMEOUT when enrolling",
131               resultCode
132             )
133           } else {
134             navigationViewModel.onEnrollFirstFailure(
135               "Incorrect resultCode or data was null",
136               resultCode
137             )
138           }
139         } else {
140           val token = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
141           val challenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
142           navigationViewModel.onEnrollFirst(token, challenge)
143         }
144       }
145     }
146 
147   override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error) {
148     Toast.makeText(activity, authAttemptViewModel.message, Toast.LENGTH_SHORT).show()
149   }
150 
151   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
152     // This is needed to support ChooseLockSettingBuilder...show(). All other activity
153     // calls should use the registerForActivity method call.
154     super.onActivityResult(requestCode, resultCode, data)
155     onConfirmDevice(resultCode, data)
156   }
157 
158   override fun onCreate(icicle: Bundle?) {
159     super.onCreate(icicle)
160 
161     if (icicle != null) {
162       Log.d(TAG, "onCreateWithSavedState")
163     } else {
164       Log.d(TAG, "onCreate()")
165     }
166 
167     if (
168       !FeatureFlagUtils.isEnabled(
169         context,
170         FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
171       )
172     ) {
173       Log.d(TAG, "Finishing due to feature not being enabled")
174       finish()
175       return
176     }
177 
178     val context = requireContext()
179     val userId = context.userId
180 
181     preferenceScreen.isVisible = false
182 
183     val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager
184 
185     val backgroundDispatcher = Dispatchers.IO
186     val activity = requireActivity()
187     val userHandle = activity.user.identifier
188 
189     val interactor =
190       FingerprintManagerInteractorImpl(
191         context.applicationContext,
192         backgroundDispatcher,
193         fingerprintManager,
194         GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext))
195       ) {
196         var toReturn: Int =
197           Secure.getIntForUser(
198             context.contentResolver,
199             Secure.SFPS_PERFORMANT_AUTH_ENABLED,
200             -1,
201             userHandle,
202           )
203         if (toReturn == -1) {
204           toReturn =
205             if (
206               context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault)
207             ) {
208               1
209             } else {
210               0
211             }
212           Secure.putIntForUser(
213             context.contentResolver,
214             Secure.SFPS_PERFORMANT_AUTH_ENABLED,
215             toReturn,
216             userHandle
217           )
218         }
219 
220         toReturn == 1
221       }
222 
223     val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
224     val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
225 
226     navigationViewModel =
227       ViewModelProvider(
228         this,
229         FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
230           userId,
231           interactor,
232           backgroundDispatcher,
233           token,
234           challenge
235         )
236       )[FingerprintSettingsNavigationViewModel::class.java]
237 
238     settingsViewModel =
239       ViewModelProvider(
240         this,
241         FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
242           userId,
243           interactor,
244           backgroundDispatcher,
245           navigationViewModel,
246         )
247       )[FingerprintSettingsViewModel::class.java]
248 
249     FingerprintSettingsViewBinder.bind(
250       this,
251       settingsViewModel,
252       navigationViewModel,
253       lifecycleScope,
254     )
255   }
256 
257   override fun getMetricsCategory(): Int {
258     return SettingsEnums.FINGERPRINT
259   }
260 
261   override fun getPreferenceScreenResId(): Int {
262     return R.xml.security_settings_fingerprint_limbo
263   }
264 
265   override fun getLogTag(): String {
266     return TAG
267   }
268 
269   override fun onStop() {
270     super.onStop()
271     navigationViewModel.maybeFinishActivity(requireActivity().isChangingConfigurations)
272   }
273 
274   override fun onPause() {
275     super.onPause()
276     settingsViewModel.shouldAuthenticate(false)
277     val transaction = parentFragmentManager.beginTransaction()
278     for (frag in parentFragmentManager.fragments) {
279       if (frag is InstrumentedDialogFragment) {
280         Log.d(TAG, "removing dialog settings fragment $frag")
281         frag.dismiss()
282         transaction.remove(frag)
283       }
284     }
285     transaction.commit()
286   }
287 
288   override fun onResume() {
289     super.onResume()
290     settingsViewModel.shouldAuthenticate(true)
291   }
292 
293   /** Used to indicate that preference has been clicked */
294   fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
295     Log.d(TAG, "onPrefClicked(${fingerprintViewModel})")
296     settingsViewModel.onPrefClicked(fingerprintViewModel)
297   }
298 
299   /** Used to indicate that a delete pref has been clicked */
300   fun onDeletePrefClicked(fingerprintViewModel: FingerprintViewModel) {
301     Log.d(TAG, "onDeletePrefClicked(${fingerprintViewModel})")
302     settingsViewModel.onDeleteClicked(fingerprintViewModel)
303   }
304 
305   override fun showSettings(state: FingerprintStateViewModel) {
306     val category =
307       this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
308         as PreferenceCategory?
309 
310     category?.removeAll()
311 
312     state.fingerprintViewModels.forEach { fingerprint ->
313       category?.addPreference(
314         FingerprintSettingsPreference(
315           requireContext(),
316           fingerprint,
317           this@FingerprintSettingsV2Fragment,
318           state.fingerprintViewModels.size == 1,
319         )
320       )
321     }
322     category?.isVisible = true
323 
324     createFingerprintsFooterPreference(state.canEnroll, state.maxFingerprints)
325     preferenceScreen.isVisible = true
326 
327     val sideFpsPref =
328       this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_SIDE_FPS_CATEGORY)
329         as PreferenceCategory?
330     sideFpsPref?.isVisible = false
331 
332     if (state.hasSideFps) {
333       sideFpsPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
334       val otherPref =
335         this@FingerprintSettingsV2Fragment.findPreference(
336           KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH
337         ) as Preference?
338       otherPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
339     }
340     addFooter(state.hasSideFps)
341   }
342   private fun addFooter(hasSideFps: Boolean) {
343     val footer =
344       this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_FOOTER)
345         as PreferenceCategory?
346     val admin =
347       RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
348         activity,
349         DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT,
350         requireActivity().userId
351       )
352     val activity = requireActivity()
353     val helpIntent =
354       HelpUtils.getHelpIntent(activity, getString(helpResource), activity::class.java.name)
355     val learnMoreClickListener =
356       View.OnClickListener { v: View? -> activity.startActivityForResult(helpIntent, 0) }
357 
358     class FooterColumn {
359       var title: CharSequence? = null
360       var learnMoreOverrideText: CharSequence? = null
361       var learnMoreOnClickListener: View.OnClickListener? = null
362     }
363 
364     var footerColumns = mutableListOf<FooterColumn>()
365     if (admin != null) {
366       val devicePolicyManager = getSystemService(DevicePolicyManager::class.java)
367       val column1 = FooterColumn()
368       column1.title =
369         devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED_EXPLANATION) {
370           getString(R.string.security_fingerprint_disclaimer_lockscreen_disabled_1)
371         }
372 
373       column1.learnMoreOnClickListener =
374         View.OnClickListener { _ ->
375           RestrictedLockUtils.sendShowAdminSupportDetailsIntent(activity, admin)
376         }
377       column1.learnMoreOverrideText = getText(R.string.admin_support_more_info)
378       footerColumns.add(column1)
379       val column2 = FooterColumn()
380       column2.title = getText(R.string.security_fingerprint_disclaimer_lockscreen_disabled_2)
381       if (hasSideFps) {
382         column2.learnMoreOverrideText =
383           getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
384       }
385       column2.learnMoreOnClickListener = learnMoreClickListener
386       footerColumns.add(column2)
387     } else {
388       val column = FooterColumn()
389       column.title =
390         getString(
391           R.string.security_settings_fingerprint_enroll_introduction_v3_message,
392           DeviceHelper.getDeviceName(requireActivity())
393         )
394       column.learnMoreOnClickListener = learnMoreClickListener
395       if (hasSideFps) {
396         column.learnMoreOverrideText =
397           getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
398       }
399       footerColumns.add(column)
400     }
401 
402     footer?.removeAll()
403     for (i in 0 until footerColumns.size) {
404       val column = footerColumns[i]
405       val footerPrefToAdd: FooterPreference =
406         FooterPreference.Builder(requireContext()).setTitle(column.title).build()
407       if (i > 0) {
408         footerPrefToAdd.setIconVisibility(View.GONE)
409       }
410       if (column.learnMoreOnClickListener != null) {
411         footerPrefToAdd.setLearnMoreAction(column.learnMoreOnClickListener)
412         if (!TextUtils.isEmpty(column.learnMoreOverrideText)) {
413           footerPrefToAdd.setLearnMoreText(column.learnMoreOverrideText)
414         }
415       }
416       footer?.addPreference(footerPrefToAdd)
417     }
418   }
419 
420   override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean {
421     Log.d(TAG, "showing delete dialog for (${fingerprintViewModel})")
422 
423     try {
424       val willDelete =
425         fingerprintPreferences()
426           .first { it?.fingerprintViewModel == fingerprintViewModel }
427           ?.askUserToDeleteDialog()
428           ?: false
429       if (willDelete) {
430         mMetricsFeatureProvider.action(
431           context,
432           SettingsEnums.ACTION_FINGERPRINT_DELETE,
433           fingerprintViewModel.fingerId
434         )
435       }
436       return willDelete
437     } catch (exception: Exception) {
438       Log.d(TAG, "askUserToDeleteDialog exception $exception")
439       return false
440     }
441   }
442 
443   override suspend fun askUserToRenameDialog(
444     fingerprintViewModel: FingerprintViewModel
445   ): Pair<FingerprintViewModel, String>? {
446     Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})")
447     try {
448       val toReturn =
449         fingerprintPreferences()
450           .first { it?.fingerprintViewModel == fingerprintViewModel }
451           ?.askUserToRenameDialog()
452       if (toReturn != null) {
453         mMetricsFeatureProvider.action(
454           context,
455           SettingsEnums.ACTION_FINGERPRINT_RENAME,
456           toReturn.first.fingerId
457         )
458       }
459       return toReturn
460     } catch (exception: Exception) {
461       Log.d(TAG, "askUserToRenameDialog exception $exception")
462       return null
463     }
464   }
465 
466   override suspend fun highlightPref(fingerId: Int) {
467     fingerprintPreferences()
468       .first { pref -> pref?.fingerprintViewModel?.fingerId == fingerId }
469       ?.highlight()
470   }
471 
472   override fun launchConfirmOrChooseLock(userId: Int) {
473     lifecycleScope.launch(Dispatchers.Default) {
474       navigationViewModel.setStepToLaunched()
475       val intent = Intent()
476       val builder =
477         ChooseLockSettingsHelper.Builder(requireActivity(), this@FingerprintSettingsV2Fragment)
478       val launched =
479         builder
480           .setRequestCode(CONFIRM_REQUEST)
481           .setTitle(getString(R.string.security_settings_fingerprint_preference_title))
482           .setRequestGatekeeperPasswordHandle(true)
483           .setUserId(userId)
484           .setForegroundOnly(true)
485           .setReturnCredentials(true)
486           .show()
487       if (!launched) {
488         intent.setClassName(SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name)
489         intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true)
490         intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
491         intent.putExtra(Intent.EXTRA_USER_ID, userId)
492         confirmDeviceResultListener.launch(intent)
493       }
494     }
495   }
496 
497   override fun launchFullFingerprintEnrollment(
498     userId: Int,
499     gateKeeperPasswordHandle: Long?,
500     challenge: Long?,
501     challengeToken: ByteArray?,
502   ) {
503     navigationViewModel.setStepToLaunched()
504     Log.d(TAG, "launchFullFingerprintEnrollment")
505     val intent = Intent()
506     intent.setClassName(
507       SETTINGS_PACKAGE_NAME,
508       FingerprintEnrollIntroductionInternal::class.java.name
509     )
510     intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true)
511     intent.putExtra(
512       SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
513       SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
514     )
515 
516     intent.putExtra(Intent.EXTRA_USER_ID, userId)
517 
518     if (gateKeeperPasswordHandle != null) {
519       intent.putExtra(EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
520     } else {
521       intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
522       intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
523     }
524     launchFirstEnrollmentListener.launch(intent)
525   }
526 
527   override fun setResultExternal(resultCode: Int) {
528     setResult(resultCode)
529   }
530 
531   override fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
532     navigationViewModel.setStepToLaunched()
533     val intent = Intent()
534     intent.setClassName(
535       SETTINGS_PACKAGE_NAME,
536       FingerprintEnrollEnrolling::class.qualifiedName.toString()
537     )
538     intent.putExtra(Intent.EXTRA_USER_ID, userId)
539     intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
540     launchAdditionalFingerprintListener.launch(intent)
541   }
542 
543   private fun onConfirmDevice(resultCode: Int, data: Intent?) {
544     val wasSuccessful = resultCode == RESULT_FINISHED || resultCode == Activity.RESULT_OK
545     val gateKeeperPasswordHandle = data?.getExtra(EXTRA_KEY_GK_PW_HANDLE) as Long?
546     lifecycleScope.launch {
547       navigationViewModel.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
548     }
549   }
550 
551   private fun createFingerprintsFooterPreference(canEnroll: Boolean, maxFingerprints: Int) {
552     val pref = this@FingerprintSettingsV2Fragment.findPreference<Preference>(KEY_FINGERPRINT_ADD)
553     val maxSummary = context?.getString(R.string.fingerprint_add_max, maxFingerprints) ?: ""
554     pref?.summary = maxSummary
555     pref?.isEnabled = canEnroll
556     pref?.setOnPreferenceClickListener {
557       navigationViewModel.onAddFingerprintClicked()
558       true
559     }
560     pref?.isVisible = true
561   }
562 
563   private fun fingerprintPreferences(): List<FingerprintSettingsPreference?> {
564     val category =
565       this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
566         as PreferenceCategory?
567 
568     return category?.let { cat ->
569       cat.childrenToList().map { it as FingerprintSettingsPreference? }
570     }
571       ?: emptyList()
572   }
573 
574   private fun PreferenceCategory.childrenToList(): List<Preference> {
575     val mutable: MutableList<Preference> = mutableListOf()
576     for (i in 0 until this.preferenceCount) {
577       mutable.add(this.getPreference(i))
578     }
579     return mutable.toList()
580   }
581 }
582