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
+
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.