bitsy-wallet/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseSecurityLockDialog.kt

189 lines
7.9 KiB
Kotlin

package cy.agorise.bitsybitshareswallet.fragments
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import android.view.ViewGroup
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.disposables.CompositeDisposable
import kotlin.math.roundToInt
/**
* Encapsulates the shared logic required for the PIN and Pattern Security Lock Fragments.
*/
abstract class BaseSecurityLockDialog : DialogFragment() {
companion object {
/** Used to denote that the user is in the step of creating the preferred security lock option */
const val STEP_SECURITY_LOCK_CREATE = 1
/** Used to denote that the user is in the step of confirming the newly created security lock option,
* this option should only be used internally */
const val STEP_SECURITY_LOCK_CONFIRM = 2
/** Used to denote that the user is in the step of verifying the current security lock option, to give
* permission to do a security constrained action like sending a transaction or trying to change the
* current security lock option */
const val STEP_SECURITY_LOCK_VERIFY = 3
/** Used to let the dialog know if the user wants to create a new PIN/Pattern or just verify its correctness
* to get access to security constrained actions */
const val KEY_STEP_SECURITY_LOCK = "key_step_security_lock"
/** The calling fragment can be calling this dialog to unlock many different things, this variable helps to
* keep track of what action the calling fragment wants to achieve */
const val KEY_ACTION_IDENTIFIER = "key_action_identifier"
}
// Container Fragment must implement this interface
interface OnPINPatternEnteredListener {
fun onPINPatternEntered(actionIdentifier: Int)
fun onPINPatternChanged()
}
/** Callback used to notify the parent that a PIN/Pattern has been entered successfully */
protected var mCallback: OnPINPatternEnteredListener? = null
/** Keeps track of the current step, can be create, confirm of verify */
protected var currentStep: Int = -1
/** Used so the calling object knows what was the intention to ask for the Security Lock */
protected var actionIdentifier: Int = -1
/** Keeps track of all RxJava disposables, to make sure they are all disposed when the fragment is destroyed */
protected var mDisposables = CompositeDisposable()
/** Current hashed version of the salt + PIN/Pattern */
protected var currentHashedPINPattern: String? = null
/** Salt used to hash the current PIN/Pattern */
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?) {
super.onViewCreated(view, savedInstanceState)
onAttachToParentFragment(parentFragment)
currentHashedPINPattern = PreferenceManager.getDefaultSharedPreferences(context)
.getString(Constants.KEY_HASHED_PIN_PATTERN, "")
currentPINPatternSalt = PreferenceManager.getDefaultSharedPreferences(context)
.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
actionIdentifier = arguments?.getInt(KEY_ACTION_IDENTIFIER) ?: -1
}
/**
* Attaches the current [DialogFragment] to its [Fragment] parent, to initialize the
* [OnPINPatternEnteredListener] interface
*/
private fun onAttachToParentFragment(fragment: Fragment?) {
try {
mCallback = fragment as OnPINPatternEnteredListener
} catch (e: ClassCastException) {
throw ClassCastException("$fragment must implement OnFilterOptionsSelectedListener")
}
}
/**
* 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)
}
}
/**
* 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)
}
}
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() {
super.onResume()
// Force dialog fragment to use the full width of the screen
val dialogWindow = dialog?.window
dialogWindow?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
override fun onDestroy() {
super.onDestroy()
if (!mDisposables.isDisposed) mDisposables.dispose()
mCountDownTimer?.cancel()
mCountDownTimer = null
}
}