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