From 7b1fb8a4bf04d6d7e60bff9068ebc0e6ba4d8c2e Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 4 Dec 2025 11:32:07 +0100 Subject: [PATCH] fix: save instance state in protected view #2283 --- .../keepass/view/ProtectedFieldView.kt | 8 +- .../keepass/view/ProtectedTextFieldView.kt | 115 ++++++++++++++++++ .../keepass/view/TemplateAbstractView.kt | 21 +++- .../keepass/view/TemplateEditView.kt | 9 +- .../kunzisoft/keepass/view/TemplateView.kt | 9 +- .../keepass/view/TextEditFieldView.kt | 36 +++--- .../kunzisoft/keepass/view/TextFieldView.kt | 41 +++---- 7 files changed, 189 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/view/ProtectedTextFieldView.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt index 29a0051b3..6bd6eba9e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt @@ -3,8 +3,12 @@ package com.kunzisoft.keepass.view import android.view.View.OnClickListener interface ProtectedFieldView { - fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?) + fun setProtection( + protection: Boolean, + isCurrentlyProtected: Boolean, + onUnprotectClickListener: OnClickListener? + ) + fun isCurrentlyProtected(): Boolean fun protect() fun unprotect() - fun isCurrentlyProtected(): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ProtectedTextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/ProtectedTextFieldView.kt new file mode 100644 index 000000000..c48f0b20f --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/ProtectedTextFieldView.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2025 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator +import android.util.AttributeSet +import android.widget.RelativeLayout +import com.kunzisoft.keepass.utils.readBooleanCompat +import com.kunzisoft.keepass.utils.writeBooleanCompat + + +abstract class ProtectedTextFieldView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) + : RelativeLayout(context, attrs, defStyle), + GenericTextFieldView, ProtectedFieldView { + + var isProtected: Boolean = false + private set + private var mIsCurrentlyProtected: Boolean = true + + // Only to fix rebuild view from template + var onSaveInstanceState: (() -> Unit)? = null + + override fun isCurrentlyProtected(): Boolean { + return mIsCurrentlyProtected + } + + override fun protect() { + mIsCurrentlyProtected = true + changeProtectedValueParameters() + } + + override fun unprotect() { + mIsCurrentlyProtected = false + changeProtectedValueParameters() + } + + override fun setProtection( + protection: Boolean, + isCurrentlyProtected: Boolean, + onUnprotectClickListener: OnClickListener? + ) { + this.isProtected = protection + this.mIsCurrentlyProtected = isCurrentlyProtected + if (isProtected) { + changeProtectedValueParameters() + } + } + + protected abstract fun changeProtectedValueParameters() + + override fun onSaveInstanceState(): Parcelable? { + onSaveInstanceState?.invoke() + return ProtectionState(super.onSaveInstanceState()).apply { + this.isCurrentlyProtected = isCurrentlyProtected() + } + } + + override fun onRestoreInstanceState(state: Parcelable?) { + when (state) { + is ProtectionState -> { + super.onRestoreInstanceState(state.superState) + mIsCurrentlyProtected = state.isCurrentlyProtected + } + else -> super.onRestoreInstanceState(state) + } + } + + internal class ProtectionState : BaseSavedState { + + var isCurrentlyProtected: Boolean = true + + constructor(superState: Parcelable?) : super(superState) + + private constructor(parcel: Parcel) : super(parcel) { + isCurrentlyProtected = parcel.readBooleanCompat() + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeBooleanCompat(isCurrentlyProtected) + } + + companion object CREATOR : Creator { + override fun createFromParcel(parcel: Parcel): ProtectionState { + return ProtectionState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt index c0823d230..b3859507d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt @@ -31,6 +31,7 @@ import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard +import com.kunzisoft.keepass.utils.readListCompat import com.kunzisoft.keepass.utils.readParcelableCompat @@ -45,6 +46,9 @@ abstract class TemplateAbstractView< private var mTemplate: Template? = null protected var mEntryInfo: EntryInfo? = null + // To keep unprotected views during orientation change + protected var mUnprotectedFields = mutableListOf() + private var mViewFields = mutableListOf() protected var mFontInVisibility: Boolean = PreferencesUtil.fieldFontIsInVisibility(context) @@ -569,7 +573,7 @@ abstract class TemplateAbstractView< } return if (!isStandardFieldName(customField.name)) { - customFieldsContainerView.visibility = View.VISIBLE + customFieldsContainerView.visibility = VISIBLE if (getIndexViewFieldByName(customField.name) >= 0) { // Update a custom field with a new value, // new field name must be the same as old field name @@ -674,6 +678,16 @@ abstract class TemplateAbstractView< putCustomField(Field(otpField.name, otpField.protectedValue)) } + fun saveUnprotectedFieldState(field: Field, isCurrentlyProtected: Boolean) { + try { + if (!isCurrentlyProtected) { + mUnprotectedFields.add(field) + } else { + mUnprotectedFields.remove(field) + } + } catch (_: Exception) {} + } + override fun onRestoreInstanceState(state: Parcelable?) { //begin boilerplate code so parent classes can restore state if (state !is SavedState) { @@ -682,6 +696,7 @@ abstract class TemplateAbstractView< } else { mTemplate = state.template mEntryInfo = state.entryInfo + mUnprotectedFields = state.unprotectedFields onRestoreEntryInstanceState(state) buildTemplateAndPopulateInfo() super.onRestoreInstanceState(state.superState) @@ -697,6 +712,7 @@ abstract class TemplateAbstractView< retrieveDefaultValues = false) saveState.template = this.mTemplate saveState.entryInfo = this.mEntryInfo + saveState.unprotectedFields = this.mUnprotectedFields onSaveEntryInstanceState(saveState) return saveState } @@ -706,6 +722,7 @@ abstract class TemplateAbstractView< protected class SavedState : BaseSavedState { var template: Template? = null var entryInfo: EntryInfo? = null + var unprotectedFields = mutableListOf() // TODO Move var tempDateTimeViewId: Int? = null @@ -714,6 +731,7 @@ abstract class TemplateAbstractView< private constructor(parcel: Parcel) : super(parcel) { template = parcel.readParcelableCompat() ?: template entryInfo = parcel.readParcelableCompat() ?: entryInfo + parcel.readListCompat(unprotectedFields) val dateTimeViewId = parcel.readInt() if (dateTimeViewId != -1) tempDateTimeViewId = dateTimeViewId @@ -723,6 +741,7 @@ abstract class TemplateAbstractView< super.writeToParcel(out, flags) out.writeParcelable(template, flags) out.writeParcelable(entryInfo, flags) + out.writeList(unprotectedFields) out.writeInt(tempDateTimeViewId ?: -1) } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt index 041b277e2..33b92c81a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt @@ -127,9 +127,16 @@ class TemplateEditView @JvmOverloads constructor(context: Context, PasswordTextEditFieldView(it) else TextEditFieldView(it)).apply { // hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout - setProtection(field.protectedValue.isProtected) { + setProtection( + protection = field.protectedValue.isProtected, + isCurrentlyProtected = mUnprotectedFields.contains(field).not() + ) { mOnUnprotectClickListener?.invoke(field, this) } + // Trick to bypass the onSaveInstanceState in rebuild child + onSaveInstanceState = { + saveUnprotectedFieldState(field, isCurrentlyProtected()) + } default = templateAttribute.default setMaxChars(templateAttribute.options.getNumberChars()) setMaxLines(templateAttribute.options.getNumberLines()) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt index 4aea2690e..ca5ac4d83 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt @@ -63,9 +63,16 @@ class TemplateView @JvmOverloads constructor(context: Context, PasskeyTextFieldView(it) else TextFieldView(it)).apply { applyFontVisibility(mFontInVisibility) - setProtection(field.protectedValue.isProtected) { + setProtection( + protection = field.protectedValue.isProtected, + isCurrentlyProtected = mUnprotectedFields.contains(field).not() + ) { mOnUnprotectClickListener?.invoke(this) } + // Trick to bypass the onSaveInstanceState in rebuild child + onSaveInstanceState = { + saveUnprotectedFieldState(field, isCurrentlyProtected()) + } label = templateAttribute.alias ?: TemplateField.getLocalizedName(context, field.name) setMaxChars(templateAttribute.options.getNumberChars()) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt index 8c742d24b..c28f116b7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt @@ -13,7 +13,6 @@ import android.util.TypedValue import android.view.View import android.view.inputmethod.EditorInfo import android.widget.LinearLayout -import android.widget.RelativeLayout import androidx.annotation.DrawableRes import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.widget.AppCompatImageButton @@ -27,8 +26,7 @@ import com.kunzisoft.keepass.R open class TextEditFieldView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : RelativeLayout(context, attrs, defStyle), - GenericTextFieldView, ProtectedFieldView { + : ProtectedTextFieldView(context, attrs, defStyle) { private var labelViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId() @@ -171,30 +169,30 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context, } } - override fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?) { - if (protection) { + override fun setProtection( + protection: Boolean, + isCurrentlyProtected: Boolean, + onUnprotectClickListener: OnClickListener? + ) { + super.setProtection(protection, isCurrentlyProtected, onUnprotectClickListener) + if (isProtected) { labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE - valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD - /* // FIXME Called by itself during orientation change + /* labelView.setEndIconOnClickListener { onUnprotectClickListener?.onClick(this@TextEditFieldView) }*/ } } - override fun isCurrentlyProtected(): Boolean { - return valueView.transformationMethod == PasswordTransformationMethod.getInstance() - } - - override fun protect() { - valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD - valueView.transformationMethod = PasswordTransformationMethod.getInstance() - } - - override fun unprotect() { - valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD - valueView.transformationMethod = SingleLineTransformationMethod.getInstance() + override fun changeProtectedValueParameters() { + if (isCurrentlyProtected()) { + valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD + valueView.transformationMethod = PasswordTransformationMethod.getInstance() + } else { + valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + valueView.transformationMethod = SingleLineTransformationMethod.getInstance() + } } override fun setOnActionClickListener(onActionClickListener: OnClickListener?, diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt index 2906b498a..9a2d418c1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt @@ -26,7 +26,6 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.ContextThemeWrapper import android.view.View -import android.widget.RelativeLayout import androidx.annotation.StringRes import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatTextView @@ -42,8 +41,7 @@ import com.kunzisoft.keepass.utils.AppUtil.openExternalApp open class TextFieldView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : RelativeLayout(context, attrs, defStyle), - GenericTextFieldView, ProtectedFieldView { + : ProtectedTextFieldView(context, attrs, defStyle) { protected var labelViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId() @@ -205,38 +203,29 @@ open class TextFieldView @JvmOverloads constructor(context: Context, } } - override fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?) { - showButton.isVisible = protection - showButton.isSelected = true - showButton.setOnClickListener { - onUnprotectClickListener?.onClick(this@TextFieldView) + override fun setProtection( + protection: Boolean, + isCurrentlyProtected: Boolean, + onUnprotectClickListener: OnClickListener? + ) { + super.setProtection(protection, isCurrentlyProtected, onUnprotectClickListener) + showButton.isVisible = isProtected + if (isProtected) { + showButton.setOnClickListener { + onUnprotectClickListener?.onClick(this@TextFieldView) + } } - changeProtectedValueParameters() - invalidate() } - override fun isCurrentlyProtected(): Boolean { - return showButton.isSelected - } - - override fun protect() { - showButton.isSelected = !showButton.isSelected - changeProtectedValueParameters() - } - - override fun unprotect() { - showButton.isSelected = !showButton.isSelected - changeProtectedValueParameters() - } - - protected fun changeProtectedValueParameters() { + override fun changeProtectedValueParameters() { valueView.apply { if (showButton.isVisible) { - applyHiddenStyle(showButton.isSelected) + applyHiddenStyle(isCurrentlyProtected()) } else { linkify() } } + invalidate() } private fun linkify() {