From 2bbb40e513a58bf58add8607627659ff61a843bd Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 2 Dec 2025 19:36:46 +0100 Subject: [PATCH] fix: Add ProtectedFieldView callback #2283 --- .../keepass/activities/EntryEditActivity.kt | 59 ++++++++++++++----- .../activities/fragments/EntryEditFragment.kt | 3 + .../UserVerificationData.kt | 2 + .../keepass/view/ProtectedFieldView.kt | 10 ++++ .../keepass/view/TemplateEditView.kt | 33 +++++++---- .../keepass/view/TextEditFieldView.kt | 32 ++++++++-- .../keepass/viewmodels/EntryEditViewModel.kt | 25 +++++--- 7 files changed, 125 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index c3fe06deb..c51619f47 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -63,6 +63,8 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecia import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.TypeMode +import com.kunzisoft.keepass.credentialprovider.UserVerificationData +import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.element.Attachment @@ -101,6 +103,7 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.updateLockPaddingStart import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel +import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel import kotlinx.coroutines.launch import java.util.EnumSet import java.util.UUID @@ -129,6 +132,7 @@ class EntryEditActivity : DatabaseLockActivity(), private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null private val mColorPickerViewModel: ColorPickerViewModel by viewModels() + private val mUserVerificationViewModel: UserVerificationViewModel by viewModels() private var mAllowCustomFields = false private var mAllowOTP = false @@ -383,23 +387,48 @@ class EntryEditActivity : DatabaseLockActivity(), lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - mEntryEditViewModel.uiState.collect { uiState -> + mEntryEditViewModel.entryEditState.collect { uiState -> when (uiState) { - EntryEditViewModel.UIState.Loading -> {} - EntryEditViewModel.UIState.ShowOverwriteMessage -> { - if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) { - AlertDialog.Builder(this@EntryEditActivity) - .setTitle(R.string.warning_overwrite_data_title) - .setMessage(R.string.warning_overwrite_data_description) - .setNegativeButton(android.R.string.cancel) { _, _ -> - mEntryEditViewModel.backPressedAlreadyApproved = true - onCancelSpecialMode() - } - .setPositiveButton(android.R.string.ok) { _, _ -> - mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true - } - .create().show() + is EntryEditViewModel.EntryEditState.Loading -> {} + is EntryEditViewModel.EntryEditState.ShowOverwriteMessage -> { + AlertDialog.Builder(this@EntryEditActivity) + .setTitle(R.string.warning_overwrite_data_title) + .setMessage(R.string.warning_overwrite_data_description) + .setNegativeButton(android.R.string.cancel) { _, _ -> + mEntryEditViewModel.backPressedAlreadyApproved = true + onCancelSpecialMode() + } + .setPositiveButton(android.R.string.ok) { _, _ -> } + .create().show() + mEntryEditViewModel.actionPerformed() + } + is EntryEditViewModel.EntryEditState.RequestUnprotectField -> { + val fieldView = uiState.protectedFieldView + if (fieldView.isCurrentlyProtected()) { + checkUserVerification( + userVerificationViewModel = mUserVerificationViewModel, + dataToVerify = UserVerificationData(protectedFieldView = fieldView) + ) + } else { + fieldView.protect() } + mEntryEditViewModel.actionPerformed() + } + } + } + } + } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + mUserVerificationViewModel.userVerificationState.collect { uVState -> + when (uVState) { + is UserVerificationViewModel.UIState.Loading -> {} + is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> { + mUserVerificationViewModel.onUserVerificationReceived() + } + is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> { + uVState.dataToVerify.protectedFieldView?.unprotect() + mUserVerificationViewModel.onUserVerificationReceived() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt index dd7885ff4..5205e4a9b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt @@ -116,6 +116,9 @@ class EntryEditFragment: DatabaseFragment() { setOnForegroundColorClickListener { mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor()) } + setOnUnprotectClickListener { _, textEditFieldView -> + mEntryEditViewModel.requestUnprotectField(textEditFieldView) + } setOnCustomEditionActionClickListener { field -> mEntryEditViewModel.requestCustomFieldEdition(field) } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt index 92b51d6e8..4d1bf8a8b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt @@ -2,9 +2,11 @@ package com.kunzisoft.keepass.credentialprovider import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.element.node.NodeId +import com.kunzisoft.keepass.view.ProtectedFieldView data class UserVerificationData( val database: ContextualDatabase? = null, val entryId: NodeId<*>? = null, + val protectedFieldView: ProtectedFieldView? = null, val preferenceKey: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt new file mode 100644 index 000000000..be78ffed3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/ProtectedFieldView.kt @@ -0,0 +1,10 @@ +package com.kunzisoft.keepass.view + +import android.view.View.OnClickListener + +interface ProtectedFieldView { + fun setOnUnprotectClickListener(onUnprotectClickListener: OnClickListener?) + fun protect() + fun unprotect() + fun isCurrentlyProtected(): Boolean +} \ No newline at end of file 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 ee07349b3..35595e7e2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt @@ -18,9 +18,9 @@ import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.database.helper.getLocalizedName import com.kunzisoft.keepass.database.helper.isStandardPasswordName +import com.kunzisoft.keepass.model.AppOriginEntryField import com.kunzisoft.keepass.model.DataDate import com.kunzisoft.keepass.model.DataTime -import com.kunzisoft.keepass.model.AppOriginEntryField import com.kunzisoft.keepass.model.PasskeyEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields @@ -35,6 +35,11 @@ class TemplateEditView @JvmOverloads constructor(context: Context, @IdRes private var mTempDateTimeViewId: Int? = null + private var mOnUnprotectClickListener: ((Field, ProtectedFieldView) -> Unit)? = null + fun setOnUnprotectClickListener(listener: ((Field, ProtectedFieldView) -> Unit)?) { + this.mOnUnprotectClickListener = listener + } + private var mOnCustomEditionActionClickListener: ((Field) -> Unit)? = null fun setOnCustomEditionActionClickListener(listener: ((Field) -> Unit)?) { this.mOnCustomEditionActionClickListener = listener @@ -80,9 +85,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context, if (color != null) { backgroundColorView.background.colorFilter = BlendModeColorFilterCompat .createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP) - backgroundColorView.visibility = View.VISIBLE + backgroundColorView.visibility = VISIBLE } else { - backgroundColorView.visibility = View.GONE + backgroundColorView.visibility = GONE } } @@ -103,9 +108,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context, if (color != null) { foregroundColorView.background.colorFilter = BlendModeColorFilterCompat .createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP) - foregroundColorView.visibility = View.VISIBLE + foregroundColorView.visibility = VISIBLE } else { - foregroundColorView.visibility = View.GONE + foregroundColorView.visibility = GONE } } @@ -113,14 +118,20 @@ class TemplateEditView @JvmOverloads constructor(context: Context, headerContainerView.isVisible = true } - override fun buildLinearTextView(templateAttribute: TemplateAttribute, - field: Field): TextEditFieldView? { + override fun buildLinearTextView( + templateAttribute: TemplateAttribute, + field: Field + ): TextEditFieldView? { return context?.let { (if (TemplateField.isStandardPasswordName(context, templateAttribute.label)) PasswordTextEditFieldView(it) else TextEditFieldView(it)).apply { // hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout - setProtection(field.protectedValue.isProtected) + if (field.protectedValue.isProtected) { + setOnUnprotectClickListener { + mOnUnprotectClickListener?.invoke(field, this) + } + } default = templateAttribute.default setMaxChars(templateAttribute.options.getNumberChars()) setMaxLines(templateAttribute.options.getNumberLines()) @@ -129,7 +140,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context, textDirection = TEXT_DIRECTION_LTR } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO + importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO } } } @@ -143,7 +154,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context, default = templateAttribute.default setActionClick(templateAttribute, field, this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO + importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO } } } @@ -157,7 +168,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context, label = templateAttribute.alias ?: TemplateField.getLocalizedName(context, field.name) val fieldValue = field.protectedValue.stringValue - value = if (fieldValue.isEmpty()) templateAttribute.default else fieldValue + value = fieldValue.ifEmpty { templateAttribute.default } // TODO edition and password generator at same time when (templateAttribute.action) { TemplateAttributeAction.NONE -> { 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 1167ca2cb..8fbb20bd3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt @@ -6,6 +6,8 @@ import android.text.InputFilter import android.text.InputType import android.text.Spannable import android.text.SpannableString +import android.text.method.PasswordTransformationMethod +import android.text.method.SingleLineTransformationMethod import android.util.AttributeSet import android.util.TypedValue import android.view.View @@ -25,7 +27,8 @@ import com.kunzisoft.keepass.R open class TextEditFieldView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : RelativeLayout(context, attrs, defStyle), GenericTextFieldView { + : RelativeLayout(context, attrs, defStyle), + GenericTextFieldView, ProtectedFieldView { private var labelViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId() @@ -168,11 +171,28 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context, } } - fun setProtection(protection: Boolean) { - if (protection) { - labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE - valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD - } + override fun setOnUnprotectClickListener(onUnprotectClickListener: OnClickListener?) { + 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 setOnActionClickListener(onActionClickListener: OnClickListener?, diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt index 6b4deed54..1bad2145c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -20,6 +20,7 @@ import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.utils.IOActionTask +import com.kunzisoft.keepass.view.ProtectedFieldView import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import java.util.UUID @@ -37,7 +38,6 @@ class EntryEditViewModel: NodeEditViewModel() { // To show dialog only one time var backPressedAlreadyApproved = false - var warningOverwriteDataAlreadyApproved = false // Useful to not relaunch a current action private var actionLocked: Boolean = false @@ -81,8 +81,8 @@ class EntryEditViewModel: NodeEditViewModel() { val onBinaryPreviewLoaded : LiveData get() = _onBinaryPreviewLoaded private val _onBinaryPreviewLoaded = SingleLiveEvent() - private val mUiState = MutableStateFlow(UIState.Loading) - val uiState: StateFlow = mUiState + private val mEntryEditState = MutableStateFlow(EntryEditState.Loading) + val entryEditState: StateFlow = mEntryEditState fun loadTemplateEntry(database: ContextualDatabase?) { loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo) @@ -125,7 +125,7 @@ class EntryEditViewModel: NodeEditViewModel() { mEntryId = null _templatesEntry.value = templatesEntry if (templatesEntry?.overwrittenData == true) { - mUiState.value = UIState.ShowOverwriteMessage + mEntryEditState.value = EntryEditState.ShowOverwriteMessage } } ).execute() @@ -293,6 +293,10 @@ class EntryEditViewModel: NodeEditViewModel() { _onPasswordSelected.value = passwordField } + fun requestUnprotectField(fieldView: ProtectedFieldView) { + mEntryEditState.value = EntryEditState.RequestUnprotectField(fieldView) + } + fun requestCustomFieldEdition(customField: Field) { _requestCustomFieldEdition.value = customField } @@ -348,6 +352,10 @@ class EntryEditViewModel: NodeEditViewModel() { _onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition) } + fun actionPerformed() { + mEntryEditState.value = EntryEditState.Loading + } + data class TemplatesEntry( val isTemplate: Boolean, val templates: List