@ -4,11 +4,25 @@
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
@ -17,6 +31,8 @@ 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 {
@ -31,7 +47,11 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
val savedLoginsKey = getPreferenceKey ( R . string . pref _key _saved _logins )
findPreference < Preference > ( savedLoginsKey ) ?. setOnPreferenceClickListener {
navigateToLoginsSettingsFragment ( )
if ( Build . VERSION . SDK _INT >= M && isHardwareAvailable && hasBiometricEnrolled ) {
showBiometricPrompt ( )
} else {
verifyPinOrShowSetupWarning ( )
}
true
}
@ -54,6 +74,31 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
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 < Preference > ( syncLogins ) ?. apply {
@ -92,7 +137,93 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
}
}
private fun navigateToLoginsSettingsFragment ( ) {
@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 )
}
@ -111,4 +242,8 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
val directions = LoginsFragmentDirections . actionLoginsFragmentToTurnOnSyncFragment ( )
findNavController ( ) . navigate ( directions )
}
companion object {
const val PIN _REQUEST = 303
}
}