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" />