mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: save instance state in protected view #2283
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user