1
0
Fork 0
fenix/app/src/main/java/org/mozilla/fenix/settings/LoginsFragment.kt

300 lines
11 KiB
Kotlin

/* 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<Preference>(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<SwitchPreference>(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<Preference>(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<Preference>(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<Preference>(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<Preference>(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
}
}