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.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()
}
}
}

View File

@@ -116,6 +116,9 @@ class EntryEditFragment: DatabaseFragment() {
setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
}
setOnUnprotectClickListener { _, textEditFieldView ->
mEntryEditViewModel.requestUnprotectField(textEditFieldView)
}
setOnCustomEditionActionClickListener { 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.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
)

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.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 -> {

View File

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

View File

@@ -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<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
private val mEntryEditState = MutableStateFlow<EntryEditState>(EntryEditState.Loading)
val entryEditState: StateFlow<EntryEditState> = 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<Template>,
@@ -362,9 +370,12 @@ class EntryEditViewModel: NodeEditViewModel() {
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState {
object Loading: UIState()
object ShowOverwriteMessage: UIState()
sealed class EntryEditState {
object Loading: EntryEditState()
object ShowOverwriteMessage: EntryEditState()
data class RequestUnprotectField(
val protectedFieldView: ProtectedFieldView
): EntryEditState()
}
companion object {