300 lines
11 KiB
Kotlin
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
|
|
}
|
|
}
|