/* 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.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference 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.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import java.util.concurrent.Executors @Suppress("TooManyFunctions", "LargeClass") class LoginsFragment : PreferenceFragmentCompat(), AccountObserver { @TargetApi(M) private lateinit var biometricPromptCallback: BiometricPrompt.AuthenticationCallback @TargetApi(M) private val executor = Executors.newSingleThreadExecutor() @TargetApi(M) private lateinit var biometricPrompt: BiometricPrompt @TargetApi(M) private lateinit var promptInfo: BiometricPrompt.PromptInfo override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.logins_preferences, rootKey) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 } } biometricPrompt = BiometricPrompt(this, executor, biometricPromptCallback) promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.logins_biometric_prompt_message)) .setDeviceCredentialAllowed(true) .build() } @Suppress("ComplexMethod") override fun onResume() { super.onResume() showToolbar(getString(R.string.preferences_passwords_logins_and_passwords)) val saveLoginsSettingKey = getPreferenceKey(R.string.pref_key_save_logins_settings) findPreference(saveLoginsSettingKey)?.apply { summary = getString( if (context.settings().shouldPromptToSaveLogins) R.string.preferences_passwords_save_logins_ask_to_save else R.string.preferences_passwords_save_logins_never_save ) setOnPreferenceClickListener { navigateToSaveLoginSettingFragment() true } } val autofillPreferenceKey = getPreferenceKey(R.string.pref_key_autofill_logins) findPreference(autofillPreferenceKey)?.apply { isEnabled = context.settings().shouldPromptToSaveLogins isChecked = context.settings().shouldAutofillLogins && context.settings().shouldPromptToSaveLogins onPreferenceChangeListener = SharedPreferenceUpdater() } val savedLoginsKey = getPreferenceKey(R.string.pref_key_saved_logins) findPreference(savedLoginsKey)?.setOnPreferenceClickListener { if (Build.VERSION.SDK_INT >= M && isHardwareAvailable && hasBiometricEnrolled) { biometricPrompt.authenticate(promptInfo) } 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 { // Temporary fix for certain devices that can't use the current biometrics library // https://github.com/mozilla-mobile/fenix/issues/7603 when { Build.MANUFACTURER.toLowerCase().contains("oneplus") -> { false } 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 } } } 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() { context?.components?.analytics?.metrics?.track(Event.OpenLogins) 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) } private fun navigateToSaveLoginSettingFragment() { val directions = LoginsFragmentDirections.actionLoginsFragmentToSaveLoginSettingFragment() findNavController().navigate(directions) } companion object { const val PIN_REQUEST = 303 } }