Created the methods required to block the security lock PIN option when the user has entered incorrectly the current PIN more times than the max allowed. When that happens the text field to type the PIN gets disabled and a propper message is shown, explaining the issue and also showing the time that needs to pass until he can try again.
This commit is contained in:
parent
97d9e8bcfb
commit
2ea32af377
5 changed files with 142 additions and 2 deletions
|
@ -1,13 +1,16 @@
|
||||||
package cy.agorise.bitsybitshareswallet.fragments
|
package cy.agorise.bitsybitshareswallet.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.CountDownTimer
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import cy.agorise.bitsybitshareswallet.R
|
||||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates the shared logic required for the PIN and Pattern Security Lock Fragments.
|
* Encapsulates the shared logic required for the PIN and Pattern Security Lock Fragments.
|
||||||
|
@ -60,6 +63,16 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
|
||||||
/** Salt used to hash the current PIN/Pattern */
|
/** Salt used to hash the current PIN/Pattern */
|
||||||
protected var currentPINPatternSalt: String? = null
|
protected var currentPINPatternSalt: String? = null
|
||||||
|
|
||||||
|
/** Current count of incorrect attempts to verify the current security lock */
|
||||||
|
protected var incorrectSecurityLockAttempts = 0
|
||||||
|
|
||||||
|
/** Time of the last lock/disable due to too many incorrect attempts to verify the security lock */
|
||||||
|
protected var incorrectSecurityLockTime = 0L
|
||||||
|
|
||||||
|
/** Timer used to update the error message when the user has tried too many incorrect attempts tu enter the
|
||||||
|
* current security lock option */
|
||||||
|
private var mCountDownTimer: CountDownTimer? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
@ -71,6 +84,12 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
|
||||||
currentPINPatternSalt = PreferenceManager.getDefaultSharedPreferences(context)
|
currentPINPatternSalt = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getString(Constants.KEY_PIN_PATTERN_SALT, "")
|
.getString(Constants.KEY_PIN_PATTERN_SALT, "")
|
||||||
|
|
||||||
|
incorrectSecurityLockAttempts = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, 0)
|
||||||
|
|
||||||
|
incorrectSecurityLockTime = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, 0L)
|
||||||
|
|
||||||
currentStep = arguments?.getInt(KEY_STEP_SECURITY_LOCK) ?: -1
|
currentStep = arguments?.getInt(KEY_STEP_SECURITY_LOCK) ?: -1
|
||||||
|
|
||||||
actionIdentifier = arguments?.getInt(KEY_ACTION_IDENTIFIER) ?: -1
|
actionIdentifier = arguments?.getInt(KEY_ACTION_IDENTIFIER) ?: -1
|
||||||
|
@ -88,6 +107,68 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increases the incorrectSecurityLockAttempts counter by one and saves that value into the shared preferences
|
||||||
|
* to account for cases when the user could try to trick the app by just closing and reopening the dialog. Also,
|
||||||
|
* stores the current time, so that when the number of attempts is bigger than the maximum allowed, the security
|
||||||
|
* lock gets locked for a certain amount of time.
|
||||||
|
*/
|
||||||
|
protected fun increaseIncorrectSecurityLockAttemptsAndTime() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
incorrectSecurityLockTime = now
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
.putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, ++incorrectSecurityLockAttempts)
|
||||||
|
.putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, now)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the values of the incorrectSecurityLockAttempts and Time, both in the local variable as well as the one
|
||||||
|
* stored in the shared preferences.
|
||||||
|
*/
|
||||||
|
protected fun resetIncorrectSecurityLockAttemptsAndTime() {
|
||||||
|
incorrectSecurityLockTime = 0
|
||||||
|
incorrectSecurityLockAttempts = 0
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
.putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, incorrectSecurityLockAttempts)
|
||||||
|
.putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, incorrectSecurityLockTime)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun startContDownTimer() {
|
||||||
|
var millis = incorrectSecurityLockTime + Constants.INCORRECT_SECURITY_LOCK_COOLDOWN - System.currentTimeMillis()
|
||||||
|
millis = millis / 1000 * 1000 + 1000 // Make sure millis account for a whole second multiple
|
||||||
|
|
||||||
|
mCountDownTimer = object : CountDownTimer(millis, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
val secondsUntilFinished = millisUntilFinished / 1000
|
||||||
|
if (secondsUntilFinished < 60) {
|
||||||
|
// Less than a minute remains
|
||||||
|
val errorMessage = getString(R.string.error__security_lock_too_many_attempts_seconds,
|
||||||
|
secondsUntilFinished.toInt())
|
||||||
|
onTimerSecondPassed(errorMessage)
|
||||||
|
} else {
|
||||||
|
// At least a minute remains
|
||||||
|
val minutesUntilFinished = (secondsUntilFinished / 60.0).roundToInt()
|
||||||
|
val errorMessage = getString(R.string.error__security_lock_too_many_attempts_minutes,
|
||||||
|
minutesUntilFinished)
|
||||||
|
onTimerSecondPassed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
onTimerFinished()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun onTimerSecondPassed(errorMessage: String)
|
||||||
|
|
||||||
|
abstract fun onTimerFinished()
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
@ -100,5 +181,8 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||||
|
|
||||||
|
mCountDownTimer?.cancel()
|
||||||
|
mCountDownTimer = null
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -46,13 +46,20 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
|
||||||
|
|
||||||
if (hashedPIN == currentHashedPINPattern) {
|
if (hashedPIN == currentHashedPINPattern) {
|
||||||
// PIN is correct, proceed
|
// PIN is correct, proceed
|
||||||
|
resetIncorrectSecurityLockAttemptsAndTime()
|
||||||
tietPIN.hideKeyboard()
|
tietPIN.hideKeyboard()
|
||||||
rootView.requestFocus()
|
rootView.requestFocus()
|
||||||
dismiss()
|
dismiss()
|
||||||
mCallback?.onPINPatternEntered(actionIdentifier)
|
mCallback?.onPINPatternEntered(actionIdentifier)
|
||||||
} else {
|
} else {
|
||||||
|
increaseIncorrectSecurityLockAttemptsAndTime()
|
||||||
|
if (incorrectSecurityLockAttempts < Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) {
|
||||||
|
// Show the error only when the user has not reached the max attempts limit, because if that
|
||||||
|
// is the case another error is gonna be shown in the setupScreen() method
|
||||||
tilPIN.error = getString(R.string.error__wrong_pin)
|
tilPIN.error = getString(R.string.error__wrong_pin)
|
||||||
}
|
}
|
||||||
|
setupScreen()
|
||||||
|
}
|
||||||
|
|
||||||
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
|
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
|
||||||
// The user is trying to create a new PIN
|
// The user is trying to create a new PIN
|
||||||
|
@ -88,9 +95,11 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
|
||||||
|
|
||||||
mDisposables.add(
|
mDisposables.add(
|
||||||
tietPIN.textChanges()
|
tietPIN.textChanges()
|
||||||
|
.skipInitialValue()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (currentStep == STEP_SECURITY_LOCK_VERIFY) {
|
if (currentStep == STEP_SECURITY_LOCK_VERIFY &&
|
||||||
|
incorrectSecurityLockAttempts < Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) {
|
||||||
// Make sure the error is removed when the user types again
|
// Make sure the error is removed when the user types again
|
||||||
tilPIN.isErrorEnabled = false
|
tilPIN.isErrorEnabled = false
|
||||||
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
|
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
|
||||||
|
@ -110,6 +119,21 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
|
||||||
STEP_SECURITY_LOCK_VERIFY -> {
|
STEP_SECURITY_LOCK_VERIFY -> {
|
||||||
tvTitle.text = getString(R.string.title__re_enter_your_pin)
|
tvTitle.text = getString(R.string.title__re_enter_your_pin)
|
||||||
tvSubTitle.text = getString(R.string.msg__enter_your_pin)
|
tvSubTitle.text = getString(R.string.msg__enter_your_pin)
|
||||||
|
if (incorrectSecurityLockAttempts >= Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) {
|
||||||
|
// User has entered the PIN incorrectly too many times
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now <= incorrectSecurityLockTime + Constants.INCORRECT_SECURITY_LOCK_COOLDOWN) {
|
||||||
|
tietPIN.setText("")
|
||||||
|
tietPIN.isEnabled = false
|
||||||
|
startContDownTimer()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
resetIncorrectSecurityLockAttemptsAndTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This is not in an else statement because we also want to enable the EditText and remove the error
|
||||||
|
// when the cooldown time has been reached
|
||||||
|
tietPIN.isEnabled = true
|
||||||
tilPIN.helperText = ""
|
tilPIN.helperText = ""
|
||||||
tilPIN.isErrorEnabled = false
|
tilPIN.isErrorEnabled = false
|
||||||
}
|
}
|
||||||
|
@ -129,4 +153,12 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTimerSecondPassed(errorMessage: String) {
|
||||||
|
tilPIN.error = errorMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimerFinished() {
|
||||||
|
setupScreen()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -167,4 +167,12 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTimerSecondPassed(errorMessage: String) {
|
||||||
|
tvMessage.error = errorMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimerFinished() {
|
||||||
|
setupScreen()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -25,6 +25,20 @@ object Constants {
|
||||||
/** Key used to store the user's selected Security Lock option */
|
/** Key used to store the user's selected Security Lock option */
|
||||||
const val KEY_SECURITY_LOCK_SELECTED = "key_security_lock_selected"
|
const val KEY_SECURITY_LOCK_SELECTED = "key_security_lock_selected"
|
||||||
|
|
||||||
|
/** Maximum allowed number of incorrect attempts to input the current security lock */
|
||||||
|
const val MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS = 1 // TODO 5
|
||||||
|
|
||||||
|
/** Minimum time that the security lock options will be disabled when the user has incorrectly tried to enter
|
||||||
|
* the current security lock option more than MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS times */
|
||||||
|
const val INCORRECT_SECURITY_LOCK_COOLDOWN = 2 * 60L * 1000 // 5 seconds TODO 5L * 60 * 1000 // 5 minutes
|
||||||
|
|
||||||
|
/** Key used to store the consecutive number of times the user has incorrectly tried to enter the
|
||||||
|
* current security lock option */
|
||||||
|
const val KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS = "key_incorrect_security_lock_attempts"
|
||||||
|
|
||||||
|
/** Key used to store the time in millis when the security lock options got locked due to many incorrect attempts */
|
||||||
|
const val KEY_INCORRECT_SECURITY_LOCK_TIME = "key_incorrect_security_lock_time"
|
||||||
|
|
||||||
/** Name of the account passed to the faucet as the referrer */
|
/** Name of the account passed to the faucet as the referrer */
|
||||||
const val FAUCET_REFERRER = "agorise"
|
const val FAUCET_REFERRER = "agorise"
|
||||||
|
|
||||||
|
|
|
@ -165,5 +165,7 @@
|
||||||
<string name="error__wront_pattern">Wrong pattern</string>
|
<string name="error__wront_pattern">Wrong pattern</string>
|
||||||
<string name="text__pattern_recorded">Pattern recorded</string>
|
<string name="text__pattern_recorded">Pattern recorded</string>
|
||||||
<string name="error__connect_at_least_4_dots">Connect at least 4 dots. Try again.</string>
|
<string name="error__connect_at_least_4_dots">Connect at least 4 dots. Try again.</string>
|
||||||
|
<string name="error__security_lock_too_many_attempts_minutes">Too many incorrect attempts. Try again in %1$d minutes.</string>
|
||||||
|
<string name="error__security_lock_too_many_attempts_seconds">Too many incorrect attempts. Try again in %1$d seconds.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue