diff --git a/build.gradle b/build.gradle index 403a0d5d62..fdaef6a2f7 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { // PhotoView includeGroupByRegex 'com\\.github\\.chrisbanes' // PFLockScreen-Android - includeGroupByRegex 'com\\.github\\.ganfra' + includeGroupByRegex 'com\\.github\\.vector-im' } } maven { diff --git a/vector/build.gradle b/vector/build.gradle index 35175389c8..57fd0cc51a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -346,7 +346,7 @@ dependencies { implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" - implementation 'com.github.ganfra:PFLockScreen-Android:1.0.0-beta8' + implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta9' // Custom Tab implementation 'androidx.browser:browser:1.2.0' diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 780f0b21e2..17557b7eb3 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -396,6 +396,11 @@ SOFTWARE.
Copyright @ 2017-2018 Atlassian Pty Ltd +
  • + PFLockScreen-Android +
    + Copyright 2018, Aleksandr Nikiforov +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt b/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt
    index 348fa4da27..630b165214 100644
    --- a/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt
    +++ b/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt
    @@ -37,6 +37,25 @@ interface PinCodeStore {
         fun getEncodedPin(): String?
     
         suspend fun hasEncodedPin(): Boolean
    +
    +    fun getRemainingPinCodeAttemptsNumber(): Int
    +
    +    fun getRemainingBiometricsAttemptsNumber(): Int
    +
    +    /**
    +     * Will return the number of remaining attempts
    +     */
    +    fun onWrongPin(): Int
    +
    +    /**
    +     * Will return the number of remaining attempts
    +     */
    +    fun onWrongBiometrics(): Int
    +
    +    /**
    +     * Will reset the counters
    +     */
    +    fun resetCounters()
     }
     
     class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences: SharedPreferences) : PinCodeStore {
    @@ -48,6 +67,8 @@ class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences:
         }
     
         override suspend fun deleteEncodedPin() = withContext(Dispatchers.IO) {
    +        // Also reset the counters
    +        resetCounters()
             sharedPreferences.edit {
                 remove(ENCODED_PIN_CODE_KEY)
             }
    @@ -72,11 +93,47 @@ class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences:
             result.error == null && result.result
         }
     
    +    override fun getRemainingPinCodeAttemptsNumber(): Int {
    +        return sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT)
    +    }
    +
    +    override fun getRemainingBiometricsAttemptsNumber(): Int {
    +        return sharedPreferences.getInt(REMAINING_BIOMETRICS_ATTEMPTS_KEY, MAX_BIOMETRIC_ATTEMPTS_NUMBER_BEFORE_FORCE_PIN)
    +    }
    +
    +    override fun onWrongPin(): Int {
    +        val remaining = getRemainingPinCodeAttemptsNumber() - 1
    +        sharedPreferences.edit {
    +            putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining)
    +        }
    +        return remaining
    +    }
    +
    +    override fun onWrongBiometrics(): Int {
    +        val remaining = getRemainingBiometricsAttemptsNumber() - 1
    +        sharedPreferences.edit {
    +            putInt(REMAINING_BIOMETRICS_ATTEMPTS_KEY, remaining)
    +        }
    +        return remaining
    +    }
    +
    +    override fun resetCounters() {
    +        sharedPreferences.edit {
    +            remove(REMAINING_PIN_CODE_ATTEMPTS_KEY)
    +            remove(REMAINING_BIOMETRICS_ATTEMPTS_KEY)
    +        }
    +    }
    +
         private suspend inline fun  awaitPinCodeCallback(crossinline callback: (PFPinCodeHelperCallback) -> Unit) = suspendCoroutine> { cont ->
             callback(PFPinCodeHelperCallback { result -> cont.resume(result) })
         }
     
         companion object {
             private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY"
    +        private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY"
    +        private const val REMAINING_BIOMETRICS_ATTEMPTS_KEY = "REMAINING_BIOMETRICS_ATTEMPTS_KEY"
    +
    +        private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3
    +        private const val MAX_BIOMETRIC_ATTEMPTS_NUMBER_BEFORE_FORCE_PIN = 5
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt
    index 8976415dec..fedc705176 100644
    --- a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt
    @@ -29,6 +29,7 @@ import com.beautycoder.pflockscreen.fragments.PFLockScreenFragment
     import im.vector.app.R
     import im.vector.app.core.extensions.replaceFragment
     import im.vector.app.core.platform.VectorBaseFragment
    +import im.vector.app.core.utils.toast
     import im.vector.app.features.MainActivity
     import im.vector.app.features.MainActivityArgs
     import kotlinx.android.parcel.Parcelize
    @@ -61,7 +62,7 @@ class PinFragment @Inject constructor(
             val encodedPin = pinCodeStore.getEncodedPin() ?: return
             val authFragment = PFLockScreenFragment()
             val builder = PFFLockScreenConfiguration.Builder(requireContext())
    -                .setUseFingerprint(true)
    +                .setUseBiometric(pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0)
                     .setTitle(getString(R.string.auth_pin_confirm_to_disable_title))
                     .setClearCodeOnError(true)
                     .setMode(PFFLockScreenConfiguration.MODE_AUTH)
    @@ -69,9 +70,10 @@ class PinFragment @Inject constructor(
             authFragment.setEncodedPinCode(encodedPin)
             authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener {
                 override fun onPinLoginFailed() {
    +                onWrongPin()
                 }
     
    -            override fun onFingerprintSuccessful() {
    +            override fun onBiometricAuthSuccessful() {
                     lifecycleScope.launch {
                         pinCodeStore.deleteEncodedPin()
                         vectorBaseActivity.setResult(Activity.RESULT_OK)
    @@ -79,7 +81,13 @@ class PinFragment @Inject constructor(
                     }
                 }
     
    -            override fun onFingerprintLoginFailed() {
    +            override fun onBiometricAuthLoginFailed() {
    +                val remainingAttempts = pinCodeStore.onWrongBiometrics()
    +                if (remainingAttempts <= 0) {
    +                    // Disable Biometrics
    +                    builder.setUseBiometric(false)
    +                    authFragment.setConfiguration(builder.build())
    +                }
                 }
     
                 override fun onCodeInputSuccessful() {
    @@ -121,8 +129,12 @@ class PinFragment @Inject constructor(
         private fun showAuthFragment() {
             val encodedPin = pinCodeStore.getEncodedPin() ?: return
             val authFragment = PFLockScreenFragment()
    +        val canUseBiometrics = pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0
             val builder = PFFLockScreenConfiguration.Builder(requireContext())
    -                .setUseFingerprint(true)
    +                .setUseBiometric(true)
    +                .setAutoShowBiometric(true)
    +                .setUseBiometric(canUseBiometrics)
    +                .setAutoShowBiometric(canUseBiometrics)
                     .setTitle(getString(R.string.auth_pin_title))
                     .setLeftButton(getString(R.string.auth_pin_forgot))
                     .setClearCodeOnError(true)
    @@ -134,17 +146,26 @@ class PinFragment @Inject constructor(
             }
             authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener {
                 override fun onPinLoginFailed() {
    +                onWrongPin()
                 }
     
    -            override fun onFingerprintSuccessful() {
    +            override fun onBiometricAuthSuccessful() {
    +                pinCodeStore.resetCounters()
                     vectorBaseActivity.setResult(Activity.RESULT_OK)
                     vectorBaseActivity.finish()
                 }
     
    -            override fun onFingerprintLoginFailed() {
    +            override fun onBiometricAuthLoginFailed() {
    +                val remainingAttempts = pinCodeStore.onWrongBiometrics()
    +                if (remainingAttempts <= 0) {
    +                    // Disable Biometrics
    +                    builder.setUseBiometric(false)
    +                    authFragment.setConfiguration(builder.build())
    +                }
                 }
     
                 override fun onCodeInputSuccessful() {
    +                pinCodeStore.resetCounters()
                     vectorBaseActivity.setResult(Activity.RESULT_OK)
                     vectorBaseActivity.finish()
                 }
    @@ -152,6 +173,21 @@ class PinFragment @Inject constructor(
             replaceFragment(R.id.pinFragmentContainer, authFragment)
         }
     
    +    private fun onWrongPin() {
    +        val remainingAttempts = pinCodeStore.onWrongPin()
    +        when {
    +            remainingAttempts > 1  ->
    +                requireActivity().toast(resources.getQuantityString(R.plurals.wrong_pin_message_remaining_attempts, remainingAttempts, remainingAttempts))
    +            remainingAttempts == 1 ->
    +                requireActivity().toast(R.string.wrong_pin_message_last_remaining_attempt)
    +            else                   -> {
    +                requireActivity().toast(R.string.too_many_pin_failures)
    +                // Logout
    +                MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true))
    +            }
    +        }
    +    }
    +
         private fun displayForgotPinWarningDialog() {
             AlertDialog.Builder(requireContext())
                     .setTitle(getString(R.string.auth_pin_reset_title))
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 25ec5de33b..6108166d23 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2517,6 +2517,12 @@
     
         Push notifications are disabled
         Review your settings to enable push notifications
    +    
    +        Wrong code, %d remaining attempt
    +        Wrong code, %d remaining attempts
    +    
    +    Warning! Last remaining attempt before logout!
    +    Too many errors, you\'ve been logged out
         Choose a PIN for security
         Confirm PIN
         Failed to validate pin, please tap a new one.