diff --git a/changelog/unreleased/4871 b/changelog/unreleased/4871
new file mode 100644
index 00000000000..fea65f1f46d
--- /dev/null
+++ b/changelog/unreleased/4871
@@ -0,0 +1,6 @@
+Enhancement: Passcode and pattern screens in landscape mode
+
+Orientation restrictions defined in the manifest have been removed, and support for both
+orientations (portrait and landscape) has been added to the passcode and pattern screens.
+
+https://github.com/owncloud/android/pull/4871
diff --git a/owncloudApp/src/main/AndroidManifest.xml b/owncloudApp/src/main/AndroidManifest.xml
index 8576ebf3229..73bf0a56ddf 100644
--- a/owncloudApp/src/main/AndroidManifest.xml
+++ b/owncloudApp/src/main/AndroidManifest.xml
@@ -187,7 +187,6 @@
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 cf89b1d4223..0f1e60ae718 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
@@ -13,7 +13,7 @@
* @author Jorge Aguado Recio
*
* Copyright (C) 2011 Bartek Przybylski
- * Copyright (C) 2025 ownCloud GmbH.
+ * Copyright (C) 2026 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
@@ -32,7 +32,6 @@ package com.owncloud.android.presentation.security.passcode
import android.content.Intent
import android.os.Bundle
-import android.text.Editable
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
@@ -42,7 +41,7 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import com.owncloud.android.BuildConfig
import com.owncloud.android.R
-import com.owncloud.android.databinding.PasscodelockBinding
+import com.owncloud.android.databinding.PasscodeLockActivityBinding
import com.owncloud.android.domain.utils.Event
import com.owncloud.android.extensions.showBiometricDialog
import com.owncloud.android.extensions.showMessageInSnackbar
@@ -66,7 +65,7 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
private val biometricViewModel by viewModel()
- private var _binding: PasscodelockBinding? = null
+ private var _binding: PasscodeLockActivityBinding? = null
val binding get() = _binding!!
private lateinit var passCodeEditTexts: Array
@@ -85,9 +84,13 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
this.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
+ savedInstanceState?.let {
+ it.getString(STATE_PASSCODE)?.let { passcode -> passCodeViewModel.restorePassCode(passcode) }
+ }
+
subscribeToViewModel()
- _binding = PasscodelockBinding.inflate(layoutInflater)
+ _binding = PasscodeLockActivityBinding.inflate(layoutInflater)
// protection against screen recording
if (!BuildConfig.DEBUG) {
@@ -106,7 +109,7 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
// Allow or disallow touches with other visible windows
binding.passcodeLockLayout.filterTouchesWhenObscured =
PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(this)
- binding.explanation.filterTouchesWhenObscured =
+ binding.passcodeExplanation.filterTouchesWhenObscured =
PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(this)
inflatePasscodeTxtLine()
@@ -120,8 +123,8 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
when (intent.action) {
ACTION_CHECK -> { //When you start the app with passcode
// this is a pass code request; the user has to input the right value
- binding.header.text = getString(R.string.pass_code_enter_pass_code)
- binding.explanation.visibility = View.INVISIBLE
+ binding.passcodeHeader.text = getString(R.string.pass_code_enter_pass_code)
+ binding.passcodeExplanation.visibility = View.INVISIBLE
supportActionBar?.setDisplayHomeAsUpEnabled(false) //Don´t show the back arrow
}
@@ -131,14 +134,14 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
requestPassCodeConfirmation()
} else {
if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) {
- binding.header.text =
+ binding.passcodeHeader.text =
getString(R.string.pass_code_configure_your_pass_code_migration, passCodeViewModel.getNumberOfPassCodeDigits())
} else {
// pass code preference has just been activated in Preferences;
// will receive and confirm pass code value
- binding.header.text = getString(R.string.pass_code_configure_your_pass_code)
+ binding.passcodeHeader.text = getString(R.string.pass_code_configure_your_pass_code)
}
- binding.explanation.visibility = View.VISIBLE
+ binding.passcodeExplanation.visibility = View.VISIBLE
when {
intent.extras?.getBoolean(EXTRAS_MIGRATION) == true -> {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
@@ -158,8 +161,8 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
ACTION_REMOVE -> { // Remove password
// pass code preference has just been disabled in Preferences;
// will confirm user knows pass code, then remove it
- binding.header.text = getString(R.string.pass_code_remove_your_pass_code)
- binding.explanation.visibility = View.INVISIBLE
+ binding.passcodeHeader.text = getString(R.string.pass_code_remove_your_pass_code)
+ binding.passcodeExplanation.visibility = View.INVISIBLE
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
@@ -174,13 +177,18 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
return true
}
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(STATE_PASSCODE, passCodeViewModel.passcode.value.orEmpty())
+ }
+
override fun onBackPressed() {
PassCodeManager.onActivityStopped(this)
super.onBackPressed()
}
private fun inflatePasscodeTxtLine() {
- val layoutCode = findViewById(R.id.layout_code)
+ val layoutCode = findViewById(R.id.passcode_value)
val numberOfPasscodeDigits = (passCodeViewModel.getPassCode()?.length ?: passCodeViewModel.getNumberOfPassCodeDigits())
for (i in 0 until numberOfPasscodeDigits) {
val txt = layoutInflater.inflate(R.layout.passcode_edit_text, layoutCode, false) as EditText
@@ -239,34 +247,27 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
}
passCodeViewModel.passcode.observe(this) { passcode ->
- if (passcode.isNotEmpty()) {
- passCodeEditTexts[passcode.length - 1]?.apply {
- text = Editable.Factory.getInstance().newEditable(passcode.last().toString())
- isEnabled = false
- }
- }
-
- if (passcode.length < numberOfPasscodeDigits) {
- //Backspace
- passCodeEditTexts[passcode.length]?.apply {
- isEnabled = true
- setText("")
- requestFocus()
+ passCodeEditTexts.forEachIndexed { index, editText ->
+ editText?.apply {
+ val digit = passcode.getOrNull(index)
+ setText(digit?.toString() ?: "")
+ isEnabled = digit == null
}
}
+ passCodeEditTexts.getOrNull(passcode.length)?.requestFocus()
}
}
private fun actionCheckOk() {
// pass code accepted in request, user is allowed to access the app
- binding.error.visibility = View.INVISIBLE
+ binding.passcodeError.visibility = View.INVISIBLE
PassCodeManager.onActivityStopped(this)
finish()
}
private fun actionCheckMigration() {
- binding.error.visibility = View.INVISIBLE
+ binding.passcodeError.visibility = View.INVISIBLE
val intent = Intent(baseContext, PassCodeActivity::class.java)
intent.apply {
@@ -305,7 +306,7 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
}
private fun actionCreateNoConfirm() {
- binding.error.visibility = View.INVISIBLE
+ binding.passcodeError.visibility = View.INVISIBLE
requestPassCodeConfirmation()
}
@@ -341,10 +342,10 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
errorMessage: Int, headerMessage: String,
explanationVisibility: Int
) {
- binding.error.setText(errorMessage)
- binding.error.visibility = View.VISIBLE
- binding.header.text = headerMessage
- binding.explanation.visibility = explanationVisibility
+ binding.passcodeError.setText(errorMessage)
+ binding.passcodeError.visibility = View.VISIBLE
+ binding.passcodeHeader.text = headerMessage
+ binding.passcodeExplanation.visibility = explanationVisibility
clearBoxes()
}
@@ -354,8 +355,8 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
*/
private fun requestPassCodeConfirmation() {
clearBoxes()
- binding.header.setText(R.string.pass_code_reenter_your_pass_code)
- binding.explanation.visibility = View.INVISIBLE
+ binding.passcodeHeader.setText(R.string.pass_code_reenter_your_pass_code)
+ binding.passcodeExplanation.visibility = View.INVISIBLE
confirmingPassCode = true
}
@@ -482,5 +483,6 @@ class PassCodeActivity : AppCompatActivity(), NumberKeyboardListener, EnableBiom
const val BIOMETRIC_HAS_FAILED = "BIOMETRIC_HAS_FAILED"
+ private const val STATE_PASSCODE = "STATE_PASSCODE"
}
}
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 8ded0c2a387..0e5e46b6a4d 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
@@ -2,8 +2,9 @@
* ownCloud Android client application
*
* @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
*
- * Copyright (C) 2021 ownCloud GmbH.
+ * Copyright (C) 2026 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
@@ -121,7 +122,7 @@ class PassCodeViewModel(
resetNumberOfAttempts()
} else {
increaseNumberOfAttempts()
- passcodeString = StringBuilder()
+ clearPassCode()
_status.postValue(Status(PasscodeAction.CHECK, PasscodeType.ERROR))
}
}
@@ -131,7 +132,7 @@ class PassCodeViewModel(
removePassCode()
_status.postValue(Status(PasscodeAction.REMOVE, PasscodeType.OK))
} else {
- passcodeString = StringBuilder()
+ clearPassCode()
_status.postValue(Status(PasscodeAction.REMOVE, PasscodeType.ERROR))
}
}
@@ -140,12 +141,13 @@ class PassCodeViewModel(
// enabling pass code
if (!confirmingPassCode) {
requestPassCodeConfirmation()
+ clearPassCode()
_status.postValue(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM))
} else if (confirmPassCode()) {
setPassCode()
_status.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
} else {
- passcodeString = StringBuilder()
+ clearPassCode()
_status.postValue(Status(PasscodeAction.CREATE, PasscodeType.ERROR))
}
}
@@ -221,6 +223,16 @@ class PassCodeViewModel(
}.start()
}
+ fun restorePassCode(passcode: String) {
+ passcodeString = StringBuilder(passcode)
+ _passcode.postValue(passcode)
+ }
+
+ fun clearPassCode() {
+ passcodeString = StringBuilder()
+ _passcode.postValue("")
+ }
+
private fun loadPinFromOldFormatIfPossible(): String? {
var pinString = ""
for (i in 1..4) {
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt
index 4afc2bc51dc..3d3b0d238f7 100644
--- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt
@@ -8,7 +8,7 @@
* @author Juan Carlos Garrote Gascón
* @author Jorge Aguado Recio
*
- * Copyright (C) 2025 ownCloud GmbH.
+ * Copyright (C) 2026 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
@@ -40,7 +40,7 @@ import com.andrognito.patternlockview.utils.PatternLockUtils
import com.owncloud.android.BuildConfig
import com.owncloud.android.R
import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider
-import com.owncloud.android.databinding.ActivityPatternLockBinding
+import com.owncloud.android.databinding.PatternLockActivityBinding
import com.owncloud.android.extensions.showBiometricDialog
import com.owncloud.android.extensions.showMessageInSnackbar
import com.owncloud.android.presentation.documentsprovider.DocumentsProviderUtils.notifyDocumentsProviderRoots
@@ -59,7 +59,7 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
private val patternViewModel by viewModel()
private val biometricViewModel by viewModel()
- private var _binding: ActivityPatternLockBinding? = null
+ private var _binding: PatternLockActivityBinding? = null
val binding get() = _binding!!
private var confirmingPattern = false
@@ -75,7 +75,7 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
- _binding = ActivityPatternLockBinding.inflate(layoutInflater)
+ _binding = PatternLockActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -102,8 +102,8 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
* This block is executed when the user opens the app after setting the pattern lock
* this block takes the pattern input by the user and checks it with the pattern initially set by the user.
*/
- binding.headerPattern.text = getString(R.string.pattern_enter_pattern)
- binding.explanationPattern.visibility = View.INVISIBLE
+ binding.patternHeader.text = getString(R.string.pattern_enter_pattern)
+ binding.patternExplanation.visibility = View.INVISIBLE
supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
ACTION_REQUEST_WITH_RESULT -> {
@@ -118,13 +118,13 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
patternExpShouldVisible = savedInstanceState.getBoolean(PATTERN_EXP_VIEW_STATE)
}
if (confirmingPattern) {
- binding.headerPattern.text = headerPatternViewText
+ binding.patternHeader.text = headerPatternViewText
if (!patternExpShouldVisible) {
- binding.explanationPattern.visibility = View.INVISIBLE
+ binding.patternExplanation.visibility = View.INVISIBLE
}
} else {
- binding.headerPattern.text = getString(R.string.pattern_configure_pattern)
- binding.explanationPattern.visibility = View.VISIBLE
+ binding.patternHeader.text = getString(R.string.pattern_configure_pattern)
+ binding.patternExplanation.visibility = View.VISIBLE
if (intent.extras?.getBoolean(EXTRAS_LOCK_ENFORCED) == true) {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
} else {
@@ -136,9 +136,9 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
/**
* This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock)
*/
- binding.headerPattern.text = getString(R.string.pattern_remove_pattern)
- binding.explanationPattern.text = getString(R.string.pattern_no_longer_required)
- binding.explanationPattern.visibility = View.VISIBLE
+ binding.patternHeader.text = getString(R.string.pattern_remove_pattern)
+ binding.patternExplanation.text = getString(R.string.pattern_no_longer_required)
+ binding.patternExplanation.visibility = View.VISIBLE
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
else -> {
@@ -223,7 +223,7 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
private fun handleActionCheck() {
if (patternViewModel.checkPatternIsValid(patternValue)) {
- binding.errorPattern.visibility = View.INVISIBLE
+ binding.patternError.visibility = View.INVISIBLE
val preferencesProvider = OCSharedPreferencesProvider(applicationContext)
preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, SystemClock.elapsedRealtime())
PatternManager.onActivityStopped(this)
@@ -241,7 +241,7 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
patternViewModel.removePattern()
val result = Intent()
setResult(RESULT_OK, result)
- binding.errorPattern.visibility = View.INVISIBLE
+ binding.patternError.visibility = View.INVISIBLE
notifyDocumentsProviderRoots(applicationContext)
finish()
} else {
@@ -254,7 +254,7 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
private fun handleActionRequestWithResult() {
if (!confirmingPattern) {
- binding.errorPattern.visibility = View.INVISIBLE
+ binding.patternError.visibility = View.INVISIBLE
requestPatternConfirmation()
} else if (confirmPattern()) {
savePatternAndExit()
@@ -271,10 +271,10 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
explanationVisibility: Int
) {
patternValue = null
- binding.errorPattern.setText(errorMessage)
- binding.errorPattern.visibility = View.VISIBLE
- binding.headerPattern.setText(headerMessage)
- binding.explanationPattern.visibility = explanationVisibility
+ binding.patternError.setText(errorMessage)
+ binding.patternError.visibility = View.VISIBLE
+ binding.patternHeader.setText(headerMessage)
+ binding.patternExplanation.visibility = explanationVisibility
}
/**
@@ -282,8 +282,8 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
*/
private fun requestPatternConfirmation() {
binding.patternLockView.clearPattern()
- binding.headerPattern.setText(R.string.pattern_reenter_pattern)
- binding.explanationPattern.visibility = View.INVISIBLE
+ binding.patternHeader.setText(R.string.pattern_reenter_pattern)
+ binding.patternExplanation.visibility = View.INVISIBLE
confirmingPattern = true
}
@@ -309,8 +309,8 @@ class PatternActivity : AppCompatActivity(), EnableBiometrics {
outState.apply {
putBoolean(KEY_CONFIRMING_PATTERN, confirmingPattern)
putString(KEY_PATTERN_STRING, patternValue)
- putString(PATTERN_HEADER_VIEW_TEXT, binding.headerPattern.text.toString())
- putBoolean(PATTERN_EXP_VIEW_STATE, binding.explanationPattern.isVisible)
+ putString(PATTERN_HEADER_VIEW_TEXT, binding.patternHeader.text.toString())
+ putBoolean(PATTERN_EXP_VIEW_STATE, binding.patternExplanation.isVisible)
}
}
diff --git a/owncloudApp/src/main/res/layout-land/passcode_lock_activity.xml b/owncloudApp/src/main/res/layout-land/passcode_lock_activity.xml
new file mode 100644
index 00000000000..0a11ace7381
--- /dev/null
+++ b/owncloudApp/src/main/res/layout-land/passcode_lock_activity.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/owncloudApp/src/main/res/layout-land/pattern_lock_activity.xml b/owncloudApp/src/main/res/layout-land/pattern_lock_activity.xml
new file mode 100644
index 00000000000..8b0e0a4c65a
--- /dev/null
+++ b/owncloudApp/src/main/res/layout-land/pattern_lock_activity.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/owncloudApp/src/main/res/layout-w600dp/passcodelock.xml b/owncloudApp/src/main/res/layout-sw600dp/passcode_lock_activity.xml
similarity index 80%
rename from owncloudApp/src/main/res/layout-w600dp/passcodelock.xml
rename to owncloudApp/src/main/res/layout-sw600dp/passcode_lock_activity.xml
index 24d517ad060..9429c81f805 100644
--- a/owncloudApp/src/main/res/layout-w600dp/passcodelock.xml
+++ b/owncloudApp/src/main/res/layout-sw600dp/passcode_lock_activity.xml
@@ -2,7 +2,7 @@
ownCloud Android client application
Copyright (C) 2012 Bartek Przybylski
- Copyright (C) 2020 ownCloud GmbH.
+ Copyright (C) 2026 ownCloud GmbH.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2,
@@ -18,21 +18,21 @@
-->
+ app:layout_constraintTop_toBottomOf="@id/passcode_header" />
+ app:layout_constraintTop_toBottomOf="@id/passcode_explanation">
-
+
+ app:layout_constraintTop_toBottomOf="@id/passcode_value" />
+ app:layout_constraintTop_toBottomOf="@id/passcode_error" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/owncloudApp/src/main/res/layout/passcodelock.xml b/owncloudApp/src/main/res/layout/passcode_lock_activity.xml
similarity index 82%
rename from owncloudApp/src/main/res/layout/passcodelock.xml
rename to owncloudApp/src/main/res/layout/passcode_lock_activity.xml
index 12f4a28fc65..9a2113774e6 100644
--- a/owncloudApp/src/main/res/layout/passcodelock.xml
+++ b/owncloudApp/src/main/res/layout/passcode_lock_activity.xml
@@ -2,7 +2,7 @@
ownCloud Android client application
Copyright (C) 2012 Bartek Przybylski
- Copyright (C) 2020 ownCloud GmbH.
+ Copyright (C) 2026 ownCloud GmbH.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2,
@@ -18,7 +18,7 @@
-->
+ app:layout_constraintTop_toBottomOf="@id/passcode_header" />
-
+
+ app:layout_constraintTop_toBottomOf="@id/passcode_value" />
+ app:layout_constraintTop_toBottomOf="@id/passcode_error" />