fix: save instance state in protected view #2283

This commit is contained in:
J-Jamet
2025-12-04 11:32:07 +01:00
parent 3567fa797b
commit 7b1fb8a4bf
7 changed files with 189 additions and 50 deletions

View File

@@ -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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<ProtectionState> {
override fun createFromParcel(parcel: Parcel): ProtectionState {
return ProtectionState(parcel)
}
override fun newArray(size: Int): Array<ProtectionState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -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<Field>()
private var mViewFields = mutableListOf<ViewField>()
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<Field>()
// 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<Field>(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)
}

View File

@@ -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())

View File

@@ -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())

View File

@@ -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?,

View File

@@ -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() {