fix: Add ProtectedFieldView callback #2283

This commit is contained in:
J-Jamet
2025-12-02 19:36:46 +01:00
parent 09ef69e6ae
commit 2bbb40e513
7 changed files with 125 additions and 39 deletions

View File

@@ -63,6 +63,8 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecia
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.TypeMode 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.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment 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.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.EnumSet import java.util.EnumSet
import java.util.UUID import java.util.UUID
@@ -129,6 +132,7 @@ class EntryEditActivity : DatabaseLockActivity(),
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
private val mColorPickerViewModel: ColorPickerViewModel by viewModels() private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
private var mAllowCustomFields = false private var mAllowCustomFields = false
private var mAllowOTP = false private var mAllowOTP = false
@@ -383,23 +387,48 @@ class EntryEditActivity : DatabaseLockActivity(),
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
mEntryEditViewModel.uiState.collect { uiState -> mEntryEditViewModel.entryEditState.collect { uiState ->
when (uiState) { when (uiState) {
EntryEditViewModel.UIState.Loading -> {} is EntryEditViewModel.EntryEditState.Loading -> {}
EntryEditViewModel.UIState.ShowOverwriteMessage -> { is EntryEditViewModel.EntryEditState.ShowOverwriteMessage -> {
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) { AlertDialog.Builder(this@EntryEditActivity)
AlertDialog.Builder(this@EntryEditActivity) .setTitle(R.string.warning_overwrite_data_title)
.setTitle(R.string.warning_overwrite_data_title) .setMessage(R.string.warning_overwrite_data_description)
.setMessage(R.string.warning_overwrite_data_description) .setNegativeButton(android.R.string.cancel) { _, _ ->
.setNegativeButton(android.R.string.cancel) { _, _ -> mEntryEditViewModel.backPressedAlreadyApproved = true
mEntryEditViewModel.backPressedAlreadyApproved = true onCancelSpecialMode()
onCancelSpecialMode() }
} .setPositiveButton(android.R.string.ok) { _, _ -> }
.setPositiveButton(android.R.string.ok) { _, _ -> .create().show()
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true mEntryEditViewModel.actionPerformed()
} }
.create().show() 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()
} }
} }
} }

View File

@@ -116,6 +116,9 @@ class EntryEditFragment: DatabaseFragment() {
setOnForegroundColorClickListener { setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor()) mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
} }
setOnUnprotectClickListener { _, textEditFieldView ->
mEntryEditViewModel.requestUnprotectField(textEditFieldView)
}
setOnCustomEditionActionClickListener { field -> setOnCustomEditionActionClickListener { field ->
mEntryEditViewModel.requestCustomFieldEdition(field) mEntryEditViewModel.requestCustomFieldEdition(field)
} }

View File

@@ -2,9 +2,11 @@ package com.kunzisoft.keepass.credentialprovider
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.view.ProtectedFieldView
data class UserVerificationData( data class UserVerificationData(
val database: ContextualDatabase? = null, val database: ContextualDatabase? = null,
val entryId: NodeId<*>? = null, val entryId: NodeId<*>? = null,
val protectedFieldView: ProtectedFieldView? = null,
val preferenceKey: String? = null val preferenceKey: String? = null
) )

View File

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

View File

@@ -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.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.AppOriginEntryField
import com.kunzisoft.keepass.model.DataDate import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.AppOriginEntryField
import com.kunzisoft.keepass.model.PasskeyEntryFields import com.kunzisoft.keepass.model.PasskeyEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -35,6 +35,11 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
@IdRes @IdRes
private var mTempDateTimeViewId: Int? = null 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 private var mOnCustomEditionActionClickListener: ((Field) -> Unit)? = null
fun setOnCustomEditionActionClickListener(listener: ((Field) -> Unit)?) { fun setOnCustomEditionActionClickListener(listener: ((Field) -> Unit)?) {
this.mOnCustomEditionActionClickListener = listener this.mOnCustomEditionActionClickListener = listener
@@ -80,9 +85,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
if (color != null) { if (color != null) {
backgroundColorView.background.colorFilter = BlendModeColorFilterCompat backgroundColorView.background.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP) .createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
backgroundColorView.visibility = View.VISIBLE backgroundColorView.visibility = VISIBLE
} else { } else {
backgroundColorView.visibility = View.GONE backgroundColorView.visibility = GONE
} }
} }
@@ -103,9 +108,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
if (color != null) { if (color != null) {
foregroundColorView.background.colorFilter = BlendModeColorFilterCompat foregroundColorView.background.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP) .createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
foregroundColorView.visibility = View.VISIBLE foregroundColorView.visibility = VISIBLE
} else { } else {
foregroundColorView.visibility = View.GONE foregroundColorView.visibility = GONE
} }
} }
@@ -113,14 +118,20 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
headerContainerView.isVisible = true headerContainerView.isVisible = true
} }
override fun buildLinearTextView(templateAttribute: TemplateAttribute, override fun buildLinearTextView(
field: Field): TextEditFieldView? { templateAttribute: TemplateAttribute,
field: Field
): TextEditFieldView? {
return context?.let { return context?.let {
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label)) (if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextEditFieldView(it) PasswordTextEditFieldView(it)
else TextEditFieldView(it)).apply { else TextEditFieldView(it)).apply {
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout // hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
setProtection(field.protectedValue.isProtected) if (field.protectedValue.isProtected) {
setOnUnprotectClickListener {
mOnUnprotectClickListener?.invoke(field, this)
}
}
default = templateAttribute.default default = templateAttribute.default
setMaxChars(templateAttribute.options.getNumberChars()) setMaxChars(templateAttribute.options.getNumberChars())
setMaxLines(templateAttribute.options.getNumberLines()) setMaxLines(templateAttribute.options.getNumberLines())
@@ -129,7 +140,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
textDirection = TEXT_DIRECTION_LTR textDirection = TEXT_DIRECTION_LTR
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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 default = templateAttribute.default
setActionClick(templateAttribute, field, this) setActionClick(templateAttribute, field, this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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 label = templateAttribute.alias
?: TemplateField.getLocalizedName(context, field.name) ?: TemplateField.getLocalizedName(context, field.name)
val fieldValue = field.protectedValue.stringValue 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 // TODO edition and password generator at same time
when (templateAttribute.action) { when (templateAttribute.action) {
TemplateAttributeAction.NONE -> { TemplateAttributeAction.NONE -> {

View File

@@ -6,6 +6,8 @@ import android.text.InputFilter
import android.text.InputType import android.text.InputType
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.method.PasswordTransformationMethod
import android.text.method.SingleLineTransformationMethod
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
@@ -25,7 +27,8 @@ import com.kunzisoft.keepass.R
open class TextEditFieldView @JvmOverloads constructor(context: Context, open class TextEditFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView { : RelativeLayout(context, attrs, defStyle),
GenericTextFieldView, ProtectedFieldView {
private var labelViewId = ViewCompat.generateViewId() private var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId()
@@ -168,11 +171,28 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
} }
} }
fun setProtection(protection: Boolean) { override fun setOnUnprotectClickListener(onUnprotectClickListener: OnClickListener?) {
if (protection) { labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
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?, override fun setOnActionClickListener(onActionClickListener: OnClickListener?,

View File

@@ -20,6 +20,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.view.ProtectedFieldView
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.util.UUID import java.util.UUID
@@ -37,7 +38,6 @@ class EntryEditViewModel: NodeEditViewModel() {
// To show dialog only one time // To show dialog only one time
var backPressedAlreadyApproved = false var backPressedAlreadyApproved = false
var warningOverwriteDataAlreadyApproved = false
// Useful to not relaunch a current action // Useful to not relaunch a current action
private var actionLocked: Boolean = false private var actionLocked: Boolean = false
@@ -81,8 +81,8 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>() private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
private val mUiState = MutableStateFlow<UIState>(UIState.Loading) private val mEntryEditState = MutableStateFlow<EntryEditState>(EntryEditState.Loading)
val uiState: StateFlow<UIState> = mUiState val entryEditState: StateFlow<EntryEditState> = mEntryEditState
fun loadTemplateEntry(database: ContextualDatabase?) { fun loadTemplateEntry(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo) loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
@@ -125,7 +125,7 @@ class EntryEditViewModel: NodeEditViewModel() {
mEntryId = null mEntryId = null
_templatesEntry.value = templatesEntry _templatesEntry.value = templatesEntry
if (templatesEntry?.overwrittenData == true) { if (templatesEntry?.overwrittenData == true) {
mUiState.value = UIState.ShowOverwriteMessage mEntryEditState.value = EntryEditState.ShowOverwriteMessage
} }
} }
).execute() ).execute()
@@ -293,6 +293,10 @@ class EntryEditViewModel: NodeEditViewModel() {
_onPasswordSelected.value = passwordField _onPasswordSelected.value = passwordField
} }
fun requestUnprotectField(fieldView: ProtectedFieldView) {
mEntryEditState.value = EntryEditState.RequestUnprotectField(fieldView)
}
fun requestCustomFieldEdition(customField: Field) { fun requestCustomFieldEdition(customField: Field) {
_requestCustomFieldEdition.value = customField _requestCustomFieldEdition.value = customField
} }
@@ -348,6 +352,10 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition) _onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
} }
fun actionPerformed() {
mEntryEditState.value = EntryEditState.Loading
}
data class TemplatesEntry( data class TemplatesEntry(
val isTemplate: Boolean, val isTemplate: Boolean,
val templates: List<Template>, val templates: List<Template>,
@@ -362,9 +370,12 @@ class EntryEditViewModel: NodeEditViewModel() {
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment) data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float) data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState { sealed class EntryEditState {
object Loading: UIState() object Loading: EntryEditState()
object ShowOverwriteMessage: UIState() object ShowOverwriteMessage: EntryEditState()
data class RequestUnprotectField(
val protectedFieldView: ProtectedFieldView
): EntryEditState()
} }
companion object { companion object {