diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index 81b5d93e5856..16f6fb404980 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -409,4 +409,7 @@ default void onDarkThemeModeChanged(DarkMode mode) { boolean isAutoUploadGPlayNotificationShown(); void setAutoUploadGPlayNotificationShown(boolean value); + + int getPassCodeDelay(); + void setPassCodeDelay(int value); } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 7f229ade6d3b..75d50b9b4a2e 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -112,6 +112,8 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__AUTO_UPLOAD_GPLAY_WARNING2_SHOWN = "auto_upload_gplay_warning2_shown"; private static final String PREF__AUTO_UPLOAD_GPLAY_NOTIFICATION_SHOWN = "auto_upload_gplay_notification_shown"; + private static final String PREF__PASSCODE_DELAY_IN_SECONDS = "passcode_delay_in_seconds"; + private static final String LOG_ENTRY = "log_entry"; private final Context context; @@ -859,4 +861,14 @@ public boolean isAutoUploadGPlayNotificationShown() { public void setAutoUploadGPlayNotificationShown(boolean value) { preferences.edit().putBoolean(PREF__AUTO_UPLOAD_GPLAY_NOTIFICATION_SHOWN, value).apply(); } + + @Override + public int getPassCodeDelay() { + return preferences.getInt(PREF__PASSCODE_DELAY_IN_SECONDS, 0); + } + + @Override + public void setPassCodeDelay(int value) { + preferences.edit().putInt(PREF__PASSCODE_DELAY_IN_SECONDS, value).apply(); + } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt index 77088066ea0f..824263bca461 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt @@ -24,6 +24,7 @@ import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.di.Injectable import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.authentication.PassCodeManager import com.owncloud.android.databinding.PasscodelockBinding @@ -73,9 +74,11 @@ class PassCodeActivity : AppCompatActivity(), Injectable { private var passCodeDigits: Array = arrayOf("", "", "", "") private var confirmingPassCode = false private var changed = true // to control that only one blocks jump + private var delayInSeconds = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + delayInSeconds = preferences.passCodeDelay binding = PasscodelockBinding.inflate(layoutInflater) setContentView(binding.root) @@ -221,6 +224,7 @@ class PassCodeActivity : AppCompatActivity(), Injectable { if (ACTION_CHECK == intent.action) { if (checkPassCode()) { preferences.resetPinWrongAttempts() + preferences.passCodeDelay = 0 // / pass code accepted in request, user is allowed to access the app passCodeManager.updateLockTimestamp() @@ -274,6 +278,7 @@ class PassCodeActivity : AppCompatActivity(), Injectable { binding.header.setText(headerMessage) // TODO check if really needed binding.explanation.visibility = explanationVisibility // TODO check if really needed clearBoxes() + increaseAndSaveDelayTime() showDelay() } @@ -337,33 +342,92 @@ class PassCodeActivity : AppCompatActivity(), Injectable { finish() } - private fun showDelay() { - val delayValue = preferences.pinBruteForceDelay() + @Suppress("MagicNumber") + private fun increaseAndSaveDelayTime() { + val maxDelayTimeInSeconds = 300 + val delayIncrementation = 15 + + if (delayInSeconds < maxDelayTimeInSeconds) { + delayInSeconds += delayIncrementation + preferences.passCodeDelay = delayInSeconds + preferences.increasePinWrongAttempts() + } + } + + @Suppress("MagicNumber") + private fun getExplanationText(timeInSecond: Int): String { + return when { + timeInSecond < 60 -> resources.getQuantityString( + R.plurals.delay_message, + timeInSecond, + timeInSecond + ) + else -> { + val minutes = timeInSecond / 60 + val remainingSeconds = timeInSecond % 60 + + when { + remainingSeconds == 0 -> resources.getQuantityString( + R.plurals.delay_message_minutes, + minutes, + minutes + ) + else -> { + val minuteText = resources.getQuantityString( + R.plurals.delay_message_minutes_part, + minutes, + minutes + ) + val secondText = resources.getQuantityString( + R.plurals.delay_message_seconds_part, + remainingSeconds, + remainingSeconds + ) + + val prefixText = "$minuteText $secondText" + getString(R.string.due_to_too_many_wrong_attempts, prefixText) + } + } + } + } + } - if (delayValue <= 0) { + @Suppress("MagicNumber") + private fun showDelay() { + val pinBruteForceCount = preferences.pinBruteForceDelay() + if (pinBruteForceCount <= 0) { return } - binding.explanation.setText(R.string.brute_force_delay) - binding.explanation.visibility = View.VISIBLE - binding.txt0.isEnabled = false - binding.txt1.isEnabled = false - binding.txt2.isEnabled = false - binding.txt3.isEnabled = false - - lifecycleScope.launch(Dispatchers.IO) { - delay(delayValue * 1000L) - - launch(Dispatchers.Main) { - binding.explanation.visibility = View.INVISIBLE - binding.txt0.isEnabled = true - binding.txt1.isEnabled = true - binding.txt2.isEnabled = true - binding.txt3.isEnabled = true - - binding.txt0.requestFocus() - binding.txt0.showKeyboard() + enableInputFields(false) + + var counter = delayInSeconds + lifecycleScope.launch(Dispatchers.Main) { + while (counter != 0) { + binding.explanation.text = getExplanationText(counter) + delay(1000) + counter -= 1 } + + enableInputFields(true) + focusFirstInputField() + } + } + + private fun enableInputFields(enabled: Boolean) { + binding.run { + explanation.setVisibleIf(!enabled) + txt0.isEnabled = enabled + txt1.isEnabled = enabled + txt2.isEnabled = enabled + txt3.isEnabled = enabled + } + } + + private fun focusFirstInputField() { + binding.run { + txt0.requestFocus() + txt0.showKeyboard() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dab1375788b9..db6255845ee7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1132,7 +1132,30 @@ Signature does not match The link to your %1$s web interface when you open it in the browser. - Delayed due to too many wrong attempts + + + Delayed %d second due to too many wrong attempts + Delayed %d seconds due to too many wrong attempts + + + + Delayed %d minute due to too many wrong attempts + Delayed %d minutes due to too many wrong attempts + + + + Delayed %d minute + Delayed %d minutes + + + + %d second + %d seconds + + + %s due to too many wrong attempts + + Create Please select one template Please choose a template and enter a file name.