/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.fenix.settings import android.annotation.TargetApi import android.app.Activity.RESULT_OK import android.app.KeyguardManager import android.content.Context.KEYGUARD_SERVICE import android.content.DialogInterface import android.content.Intent import android.os.Build import android.os.Build.VERSION_CODES.M import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.manager.SyncEnginesStorage import org.mozilla.fenix.R import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import java.util.concurrent.Executors @Suppress("TooManyFunctions") class LoginsFragment : PreferenceFragmentCompat(), AccountObserver { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.logins_preferences, rootKey) } override fun onResume() { super.onResume() activity?.title = getString(R.string.preferences_passwords_logins_and_passwords) (activity as AppCompatActivity).supportActionBar?.show() val savedLoginsKey = getPreferenceKey(R.string.pref_key_saved_logins) findPreference(savedLoginsKey)?.setOnPreferenceClickListener { if (Build.VERSION.SDK_INT >= M && isHardwareAvailable && hasBiometricEnrolled) { showBiometricPrompt() } else { verifyPinOrShowSetupWarning() } true } val accountManager = requireComponents.backgroundServices.accountManager accountManager.register(this, owner = this) val accountExists = accountManager.authenticatedAccount() != null val needsReauth = accountManager.accountNeedsReauth() when { needsReauth -> updateSyncPreferenceNeedsReauth() accountExists -> updateSyncPreferenceStatus() !accountExists -> updateSyncPreferenceNeedsLogin() } } override fun onAuthenticated(account: OAuthAccount, authType: AuthType) = updateSyncPreferenceStatus() override fun onLoggedOut() = updateSyncPreferenceNeedsLogin() override fun onAuthenticationProblems() = updateSyncPreferenceNeedsReauth() val isHardwareAvailable: Boolean by lazy { if (Build.VERSION.SDK_INT >= M) { context?.let { val bm = BiometricManager.from(it) val canAuthenticate = bm.canAuthenticate() !(canAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE || canAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE) } ?: false } else { false } } val hasBiometricEnrolled: Boolean by lazy { if (Build.VERSION.SDK_INT >= M) { context?.let { val bm = BiometricManager.from(it) val canAuthenticate = bm.canAuthenticate() (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) } ?: false } else { false } } private fun updateSyncPreferenceStatus() { val syncLogins = getPreferenceKey(R.string.pref_key_password_sync_logins) findPreference(syncLogins)?.apply { val syncEnginesStatus = SyncEnginesStorage(context!!).getStatus() val loginsSyncStatus = syncEnginesStatus.getOrElse(SyncEngine.Passwords) { false } summary = getString( if (loginsSyncStatus) R.string.preferences_passwords_sync_logins_on else R.string.preferences_passwords_sync_logins_off ) setOnPreferenceClickListener { navigateToAccountSettingsFragment() true } } } private fun updateSyncPreferenceNeedsLogin() { val syncLogins = getPreferenceKey(R.string.pref_key_password_sync_logins) findPreference(syncLogins)?.apply { summary = getString(R.string.preferences_passwords_sync_logins_sign_in) setOnPreferenceClickListener { navigateToTurnOnSyncFragment() true } } } private fun updateSyncPreferenceNeedsReauth() { val syncLogins = getPreferenceKey(R.string.pref_key_password_sync_logins) findPreference(syncLogins)?.apply { summary = getString(R.string.preferences_passwords_sync_logins_reconnect) setOnPreferenceClickListener { navigateToAccountProblemFragment() true } } } @TargetApi(M) private fun showBiometricPrompt() { val biometricPromptCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { // Authentication Error } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { lifecycleScope.launch(Main) { navigateToSavedLoginsFragment() } } override fun onAuthenticationFailed() { // Authenticated Failed } } val executor = Executors.newSingleThreadExecutor() val biometricPrompt = BiometricPrompt(this, executor, biometricPromptCallback) val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.logins_biometric_prompt_message)) .setDeviceCredentialAllowed(true) .build() biometricPrompt.authenticate(promptInfo) } private fun verifyPinOrShowSetupWarning() { val manager = activity?.getSystemService(KEYGUARD_SERVICE) as KeyguardManager if (manager.isKeyguardSecure) { showPinVerification() } else { if (context?.settings()?.shouldShowSecurityPinWarning == true) { showPinDialogWarning() } else { navigateToSavedLoginsFragment() } } } private fun showPinDialogWarning() { context?.let { AlertDialog.Builder(it).apply { setTitle(getString(R.string.logins_warning_dialog_title)) setMessage( getString(R.string.logins_warning_dialog_message) ) setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ -> navigateToSavedLoginsFragment() } setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ -> it.dismiss() val intent = Intent( android.provider.Settings.ACTION_SECURITY_SETTINGS ) startActivity(intent) } create() }.show() it.settings().incrementShowLoginsSecureWarningCount() } } private fun showPinVerification() { val manager = activity?.getSystemService(KEYGUARD_SERVICE) as KeyguardManager val intent = manager.createConfirmDeviceCredentialIntent( getString(R.string.logins_biometric_prompt_message_pin), getString(R.string.logins_biometric_prompt_message) ) startActivityForResult(intent, PIN_REQUEST) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == PIN_REQUEST && resultCode == RESULT_OK) { navigateToSavedLoginsFragment() } else { super.onActivityResult(requestCode, resultCode, data) } } private fun navigateToSavedLoginsFragment() { val directions = LoginsFragmentDirections.actionLoginsFragmentToSavedLoginsFragment() findNavController().navigate(directions) } private fun navigateToAccountSettingsFragment() { val directions = LoginsFragmentDirections.actionLoginsFragmentToAccountSettingsFragment() findNavController().navigate(directions) } private fun navigateToAccountProblemFragment() { val directions = LoginsFragmentDirections.actionLoginsFragmentToAccountProblemFragment() findNavController().navigate(directions) } private fun navigateToTurnOnSyncFragment() { val directions = LoginsFragmentDirections.actionLoginsFragmentToTurnOnSyncFragment() findNavController().navigate(directions) } companion object { const val PIN_REQUEST = 303 } }