From c9d7ad4607c48b4b3e1544da6237516006e3eac2 Mon Sep 17 00:00:00 2001 From: Jesus Recio Date: Thu, 3 Sep 2026 08:48:51 +0200 Subject: [PATCH 1/4] feat: hide keyboard during countdown. After that, remove error message Signed-off-by: Jesus Recio --- .../android/presentation/security/passcode/PassCodeActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt index cb3157e39b1..b1996e9ce98 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt @@ -228,6 +228,8 @@ class PassCodeActivity : ToolbarActivity(), NumberKeyboardListener, EnableBiomet }) passCodeViewModel.getFinishedTimeToUnlockLiveData.observe(this, Event.EventObserver { binding.lockTime.visibility = View.INVISIBLE + binding.numberKeyboard.visibility = View.VISIBLE + binding.passcodeError.visibility = View.INVISIBLE for (editText: EditText? in passCodeEditTexts) { editText?.isEnabled = true } @@ -347,6 +349,7 @@ class PassCodeActivity : ToolbarActivity(), NumberKeyboardListener, EnableBiomet val timeToUnlock = passCodeViewModel.getTimeToUnlockLeft() if (timeToUnlock > 0) { binding.lockTime.visibility = View.VISIBLE + binding.numberKeyboard.visibility = View.INVISIBLE for (editText: EditText? in passCodeEditTexts) { editText?.isEnabled = false } From d33c526327285b81b770e3fb9c01e987958f0d9d Mon Sep 17 00:00:00 2001 From: Jesus Recio Date: Thu, 3 Sep 2026 09:14:00 +0200 Subject: [PATCH 2/4] chore: add calens file Signed-off-by: Jesus Recio --- changelog/unreleased/4967 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/unreleased/4967 diff --git a/changelog/unreleased/4967 b/changelog/unreleased/4967 new file mode 100644 index 00000000000..13df984c35c --- /dev/null +++ b/changelog/unreleased/4967 @@ -0,0 +1,6 @@ +Bugfix: Hide keyboard and clear error when brute-force countdown ends + +The number keyboard has been hidden during the brute-force protection countdown so users cannot interact with it. The wrong-passcode error message has been cleared once the countdown ends, leaving a clean screen for the next attempt. + +https://github.com/owncloud/android/issues/4874 +https://github.com/owncloud/android/pull/4967 From d211e25a1ba8bac59bf347628260a04a05a9f941 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Thu, 10 Sep 2026 11:19:52 +0200 Subject: [PATCH 3/4] fix: wrap passcode status with Event to handle lifecycle-aware observation Signed-off-by: Jorge Aguado Recio --- .../settings/security/PassCodeActivityTest.kt | 18 ++++++++--------- .../security/passcode/PassCodeActivity.kt | 4 ++-- .../security/passcode/PassCodeViewModel.kt | 20 +++++++++---------- .../security/PassCodeViewModelTest.kt | 14 ++++++------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt index acca5a6868d..ded6a1bd7e2 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt @@ -66,7 +66,7 @@ class PassCodeActivityTest { private lateinit var timeToUnlockLiveData: MutableLiveData> private lateinit var finishTimeToUnlockLiveData: MutableLiveData> - private lateinit var statusLiveData: MutableLiveData + private lateinit var statusLiveData: MutableLiveData> private lateinit var passcodeLiveData: MutableLiveData private lateinit var passCodeViewModel: PassCodeViewModel @@ -206,7 +206,7 @@ class PassCodeActivityTest { // Open Activity in passcode creation mode openPasscodeActivity(PassCodeActivity.ACTION_CREATE) - statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM)) + statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM))) with(R.id.header) { isDisplayed(true) @@ -224,7 +224,7 @@ class PassCodeActivityTest { // Open Activity in passcode creation mode openPasscodeActivity(PassCodeActivity.ACTION_CREATE) - statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)) + statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))) // Click dialog's enable option onView(withText(R.string.common_yes)).perform(click()) @@ -238,7 +238,7 @@ class PassCodeActivityTest { // Open Activity in passcode creation mode openPasscodeActivity(PassCodeActivity.ACTION_CREATE) - statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.ERROR)) + statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.ERROR))) with(R.id.header) { isDisplayed(true) @@ -278,7 +278,7 @@ class PassCodeActivityTest { // Open Activity in passcode deletion mode openPasscodeActivity(PassCodeActivity.ACTION_REMOVE) - statusLiveData.postValue(Status(PasscodeAction.REMOVE, PasscodeType.OK)) + statusLiveData.postValue(Event(Status(PasscodeAction.REMOVE, PasscodeType.OK))) assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK) } @@ -288,7 +288,7 @@ class PassCodeActivityTest { // Open Activity in passcode deletion mode openPasscodeActivity(PassCodeActivity.ACTION_REMOVE) - statusLiveData.postValue(Status(PasscodeAction.REMOVE, PasscodeType.ERROR)) + statusLiveData.postValue(Event(Status(PasscodeAction.REMOVE, PasscodeType.ERROR))) with(R.id.header) { isDisplayed(true) @@ -311,7 +311,7 @@ class PassCodeActivityTest { // Open Activity in passcode creation mode openPasscodeActivity(PassCodeActivity.ACTION_CREATE) - statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)) + statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))) onView(withText(R.string.biometric_dialog_title)).check(matches(isDisplayed())) onView(withText(R.string.common_yes)).check(matches(isDisplayed())) @@ -325,7 +325,7 @@ class PassCodeActivityTest { // Open Activity in passcode creation mode openPasscodeActivity(PassCodeActivity.ACTION_CREATE) - statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)) + statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))) onView(withText(R.string.common_yes)).perform(click()) @@ -340,7 +340,7 @@ class PassCodeActivityTest { // Open Activity in passcode creation mode openPasscodeActivity(PassCodeActivity.ACTION_CREATE) - statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)) + statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))) onView(withText(R.string.common_no)).perform(click()) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt index b1996e9ce98..54514131675 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt @@ -236,7 +236,7 @@ class PassCodeActivity : ToolbarActivity(), NumberKeyboardListener, EnableBiomet passCodeEditTexts.first()?.requestFocus() }) - passCodeViewModel.status.observe(this) { status -> + passCodeViewModel.status.observe(this, Event.EventObserver { status -> when (status.action) { PasscodeAction.CHECK -> { when (status.type) { @@ -262,7 +262,7 @@ class PassCodeActivity : ToolbarActivity(), NumberKeyboardListener, EnableBiomet } } } - } + }) passCodeViewModel.passcode.observe(this) { passcode -> passCodeEditTexts.forEachIndexed { index, editText -> diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt index 0e5e46b6a4d..97ea1603527 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt @@ -57,8 +57,8 @@ class PassCodeViewModel( val passcode: LiveData get() = _passcode - private var _status = MutableLiveData() - val status: LiveData + private var _status = MutableLiveData>() + val status: LiveData> get() = _status private var numberOfPasscodeDigits: Int @@ -116,24 +116,24 @@ class PassCodeViewModel( if (passCode != null && passCode.length < getNumberOfPassCodeDigits()) { setMigrationRequired(true) removePassCode() - _status.postValue(Status(PasscodeAction.CHECK, PasscodeType.MIGRATION)) + _status.postValue(Event(Status(PasscodeAction.CHECK, PasscodeType.MIGRATION))) } - _status.postValue(Status(PasscodeAction.CHECK, PasscodeType.OK)) + _status.postValue(Event(Status(PasscodeAction.CHECK, PasscodeType.OK))) resetNumberOfAttempts() } else { increaseNumberOfAttempts() clearPassCode() - _status.postValue(Status(PasscodeAction.CHECK, PasscodeType.ERROR)) + _status.postValue(Event(Status(PasscodeAction.CHECK, PasscodeType.ERROR))) } } private fun actionRemovePasscode() { if (checkPassCodeIsValid(passcodeString.toString())) { removePassCode() - _status.postValue(Status(PasscodeAction.REMOVE, PasscodeType.OK)) + _status.postValue(Event(Status(PasscodeAction.REMOVE, PasscodeType.OK))) } else { clearPassCode() - _status.postValue(Status(PasscodeAction.REMOVE, PasscodeType.ERROR)) + _status.postValue(Event(Status(PasscodeAction.REMOVE, PasscodeType.ERROR))) } } @@ -142,13 +142,13 @@ class PassCodeViewModel( if (!confirmingPassCode) { requestPassCodeConfirmation() clearPassCode() - _status.postValue(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM)) + _status.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM))) } else if (confirmPassCode()) { setPassCode() - _status.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)) + _status.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))) } else { clearPassCode() - _status.postValue(Status(PasscodeAction.CREATE, PasscodeType.ERROR)) + _status.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.ERROR))) } } diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt index 4e3f982b3eb..e7654a98944 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt @@ -143,7 +143,7 @@ class PassCodeViewModelTest : ViewModelTest() { passCodeViewModel.onNumberClicked(1) passCodeViewModel.onNumberClicked(1) - assertEquals(Status(PasscodeAction.CHECK, PasscodeType.OK), passCodeViewModel.status.value) + assertEquals(Status(PasscodeAction.CHECK, PasscodeType.OK), passCodeViewModel.status.value?.peekContent()) verify(exactly = 1) { preferencesProvider.putInt(PREFERENCE_LOCK_ATTEMPTS, 0) @@ -161,7 +161,7 @@ class PassCodeViewModelTest : ViewModelTest() { passCodeViewModel.onNumberClicked(1) passCodeViewModel.onNumberClicked(1) - assertEquals(Status(PasscodeAction.CHECK, PasscodeType.ERROR), passCodeViewModel.status.value) + assertEquals(Status(PasscodeAction.CHECK, PasscodeType.ERROR), passCodeViewModel.status.value?.peekContent()) verify(exactly = 1) { preferencesProvider.putInt(PREFERENCE_LOCK_ATTEMPTS, any()) @@ -180,7 +180,7 @@ class PassCodeViewModelTest : ViewModelTest() { passCodeViewModel.onNumberClicked(1) passCodeViewModel.onNumberClicked(1) - assertEquals(Status(PasscodeAction.REMOVE, PasscodeType.OK), passCodeViewModel.status.value) + assertEquals(Status(PasscodeAction.REMOVE, PasscodeType.OK), passCodeViewModel.status.value?.peekContent()) verify(exactly = 1) { preferencesProvider.removePreference(PREFERENCE_PASSCODE) @@ -199,7 +199,7 @@ class PassCodeViewModelTest : ViewModelTest() { passCodeViewModel.onNumberClicked(1) passCodeViewModel.onNumberClicked(1) - assertEquals(Status(PasscodeAction.REMOVE, PasscodeType.ERROR), passCodeViewModel.status.value) + assertEquals(Status(PasscodeAction.REMOVE, PasscodeType.ERROR), passCodeViewModel.status.value?.peekContent()) } @Test @@ -212,7 +212,7 @@ class PassCodeViewModelTest : ViewModelTest() { passCodeViewModel.onNumberClicked(1) passCodeViewModel.onNumberClicked(1) - assertEquals(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM), passCodeViewModel.status.value) + assertEquals(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM), passCodeViewModel.status.value?.peekContent()) } @Test @@ -231,7 +231,7 @@ class PassCodeViewModelTest : ViewModelTest() { passCodeViewModel.onNumberClicked(1) passCodeViewModel.onNumberClicked(1) - assertEquals(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM), passCodeViewModel.status.value) + assertEquals(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM), passCodeViewModel.status.value?.peekContent()) verify(exactly = 1) { preferencesProvider.putString(PREFERENCE_PASSCODE, any()) @@ -255,7 +255,7 @@ class PassCodeViewModelTest : ViewModelTest() { passCodeViewModel.onNumberClicked(1) passCodeViewModel.onNumberClicked(1) - assertEquals(Status(PasscodeAction.CREATE, PasscodeType.ERROR), passCodeViewModel.status.value) + assertEquals(Status(PasscodeAction.CREATE, PasscodeType.ERROR), passCodeViewModel.status.value?.peekContent()) } @Test From b8052f082e286cae231ff6cb6918d5b20ed57fd4 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Thu, 10 Sep 2026 13:18:03 +0200 Subject: [PATCH 4/4] fix: keep error message visible during passcode countdown across rotations Signed-off-by: Jorge Aguado Recio --- .../android/presentation/security/passcode/PassCodeActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt index 54514131675..47de950a284 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeActivity.kt @@ -348,6 +348,8 @@ class PassCodeActivity : ToolbarActivity(), NumberKeyboardListener, EnableBiomet private fun lockScreen() { val timeToUnlock = passCodeViewModel.getTimeToUnlockLeft() if (timeToUnlock > 0) { + binding.passcodeError.setText(R.string.pass_code_wrong) + binding.passcodeError.visibility = View.VISIBLE binding.lockTime.visibility = View.VISIBLE binding.numberKeyboard.visibility = View.INVISIBLE for (editText: EditText? in passCodeEditTexts) {