Add template / edit / view

This commit is contained in:
J-Jamet
2021-06-16 19:18:49 +02:00
parent f289a921f1
commit 4afbb688ba
24 changed files with 1273 additions and 1018 deletions

View File

@@ -22,11 +22,11 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.password.PasswordGenerator import com.kunzisoft.keepass.password.PasswordGenerator
@@ -80,7 +80,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
passwordView = root?.findViewById(R.id.password) passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility() passwordView?.applyFontVisibility()
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button) val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity)) passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
View.VISIBLE else View.GONE View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(activity) val clipboardHelper = ClipboardHelper(activity)
passwordCopyView?.setOnClickListener { passwordCopyView?.setOnClickListener {

View File

@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.view.DateTimeView import com.kunzisoft.keepass.view.DateTimeEditView
import org.joda.time.DateTime import org.joda.time.DateTime
class GroupEditDialogFragment : DialogFragment() { class GroupEditDialogFragment : DialogFragment() {
@@ -56,7 +56,7 @@ class GroupEditDialogFragment : DialogFragment() {
private lateinit var nameTextView: TextView private lateinit var nameTextView: TextView
private lateinit var notesTextLayoutView: TextInputLayout private lateinit var notesTextLayoutView: TextInputLayout
private lateinit var notesTextView: TextView private lateinit var notesTextView: TextView
private lateinit var expirationView: DateTimeView private lateinit var expirationView: DateTimeEditView
enum class EditGroupDialogAction { enum class EditGroupDialogAction {
CREATION, UPDATE, NONE; CREATION, UPDATE, NONE;

View File

@@ -35,14 +35,13 @@ import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.TemplateView import com.kunzisoft.keepass.view.TemplateEditView
import com.kunzisoft.keepass.view.collapse import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand import com.kunzisoft.keepass.view.expand
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
@@ -55,7 +54,7 @@ class EntryEditFragment: DatabaseFragment() {
private val mEntryEditViewModel: EntryEditViewModel by activityViewModels() private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
private lateinit var templateView: TemplateView private lateinit var templateView: TemplateEditView
private lateinit var attachmentsContainerView: ViewGroup private lateinit var attachmentsContainerView: ViewGroup
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
@@ -67,9 +66,6 @@ class EntryEditFragment: DatabaseFragment() {
.inflate(R.layout.fragment_entry_edit, container, false) .inflate(R.layout.fragment_entry_edit, container, false)
templateView = rootView.findViewById(R.id.template_view) templateView = rootView.findViewById(R.id.template_view)
templateView.populateIconMethod = { imageView, icon ->
drawFactory?.assignDatabaseIcon(imageView, icon, iconColor)
}
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container) attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list) attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
@@ -87,6 +83,9 @@ class EntryEditFragment: DatabaseFragment() {
taIconColor?.recycle() taIconColor?.recycle()
templateView.apply { templateView.apply {
populateIconMethod = { imageView, icon ->
drawFactory?.assignDatabaseIcon(imageView, icon, iconColor)
}
setOnIconClickListener { setOnIconClickListener {
mEntryEditViewModel.requestIconSelection(templateView.getIcon()) mEntryEditViewModel.requestIconSelection(templateView.getIcon())
} }
@@ -242,6 +241,7 @@ class EntryEditFragment: DatabaseFragment() {
context?.let { context -> context?.let { context ->
templateView.setFontInVisibility(PreferencesUtil.fieldFontIsInVisibility(context)) templateView.setFontInVisibility(PreferencesUtil.fieldFontIsInVisibility(context))
templateView.setHideProtectedValue(PreferencesUtil.hideProtectedValue(context))
} }
} }

View File

@@ -1,13 +1,12 @@
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -16,37 +15,25 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
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.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.UuidUtil import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.EntryFieldView import com.kunzisoft.keepass.view.EntryFieldView
import com.kunzisoft.keepass.view.TemplateView
import com.kunzisoft.keepass.viewmodels.EntryViewModel import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.* import java.util.*
class EntryFragment: DatabaseFragment() { class EntryFragment: DatabaseFragment() {
private lateinit var entryFieldsContainerView: View private lateinit var templateView: TemplateView
private lateinit var userNameFieldView: EntryFieldView
private lateinit var passwordFieldView: EntryFieldView
private lateinit var otpFieldView: EntryFieldView
private lateinit var urlFieldView: EntryFieldView
private lateinit var notesFieldView: EntryFieldView
private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup
private lateinit var expiresDateView: TextView
private lateinit var creationDateView: TextView private lateinit var creationDateView: TextView
private lateinit var modificationDateView: TextView private lateinit var modificationDateView: TextView
private lateinit var expiresImageView: ImageView
private lateinit var attachmentsContainerView: View private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
@@ -56,11 +43,6 @@ class EntryFragment: DatabaseFragment() {
private lateinit var uuidView: TextView private lateinit var uuidView: TextView
private lateinit var uuidReferenceView: TextView private lateinit var uuidReferenceView: TextView
private var mFontInVisibility: Boolean = false
private var mHideProtectedValue: Boolean = false
private var mIsFirstTimeAskAllowCopyPasswordAndProtectedFields: Boolean = false
private var mAllowCopyPasswordAndProtectedFields: Boolean = false
private var mOtpRunnable: Runnable? = null private var mOtpRunnable: Runnable? = null
private var mClipboardHelper: ClipboardHelper? = null private var mClipboardHelper: ClipboardHelper? = null
@@ -76,7 +58,7 @@ class EntryFragment: DatabaseFragment() {
return inflater.cloneInContext(contextThemed) return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry, container, false) .inflate(R.layout.fragment_entry, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -86,28 +68,7 @@ class EntryFragment: DatabaseFragment() {
attachmentsAdapter?.database = mDatabase attachmentsAdapter?.database = mDatabase
} }
entryFieldsContainerView = view.findViewById(R.id.entry_fields_container) templateView = view.findViewById(R.id.entry_template)
entryFieldsContainerView.visibility = View.GONE
userNameFieldView = view.findViewById(R.id.entry_user_name_field)
userNameFieldView.setLabel(R.string.entry_user_name)
passwordFieldView = view.findViewById(R.id.entry_password_field)
passwordFieldView.setLabel(R.string.entry_password)
otpFieldView = view.findViewById(R.id.entry_otp_field)
otpFieldView.setLabel(R.string.entry_otp)
urlFieldView = view.findViewById(R.id.entry_url_field)
urlFieldView.setLabel(R.string.entry_url)
urlFieldView.setLinkAll()
notesFieldView = view.findViewById(R.id.entry_notes_field)
notesFieldView.setLabel(R.string.entry_notes)
notesFieldView.setAutoLink()
extraFieldsContainerView = view.findViewById(R.id.extra_fields_container)
extraFieldsListView = view.findViewById(R.id.extra_fields_list)
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container) attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
attachmentsListView = view.findViewById(R.id.entry_attachments_list) attachmentsListView = view.findViewById(R.id.entry_attachments_list)
@@ -117,10 +78,8 @@ class EntryFragment: DatabaseFragment() {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
expiresDateView = view.findViewById(R.id.entry_expires_date)
creationDateView = view.findViewById(R.id.entry_created) creationDateView = view.findViewById(R.id.entry_created)
modificationDateView = view.findViewById(R.id.entry_modified) modificationDateView = view.findViewById(R.id.entry_modified)
expiresImageView = view.findViewById(R.id.entry_expires_image)
uuidContainerView = view.findViewById(R.id.entry_UUID_container) uuidContainerView = view.findViewById(R.id.entry_UUID_container)
uuidContainerView.apply { uuidContainerView.apply {
@@ -129,6 +88,10 @@ class EntryFragment: DatabaseFragment() {
uuidView = view.findViewById(R.id.entry_UUID) uuidView = view.findViewById(R.id.entry_UUID)
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference) uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
mEntryViewModel.template.observe(viewLifecycleOwner) { template ->
templateView.setTemplate(template)
}
mEntryViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo -> mEntryViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo ->
assignEntryInfo(entryInfo) assignEntryInfo(entryInfo)
} }
@@ -145,111 +108,57 @@ class EntryFragment: DatabaseFragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
loadTemplateSettings()
}
private fun loadTemplateSettings() {
context?.let { context -> context?.let { context ->
mFontInVisibility = PreferencesUtil.fieldFontIsInVisibility(context) templateView.setFontInVisibility(PreferencesUtil.fieldFontIsInVisibility(context))
mHideProtectedValue = PreferencesUtil.hideProtectedValue(context) templateView.setHideProtectedValue(PreferencesUtil.hideProtectedValue(context))
mIsFirstTimeAskAllowCopyPasswordAndProtectedFields = templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(context) templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
mAllowCopyPasswordAndProtectedFields =
PreferencesUtil.allowCopyPasswordAndProtectedFields(context)
} }
} }
private fun assignEntryInfo(entryInfo: EntryInfo?) { private fun assignEntryInfo(entryInfo: EntryInfo?) {
context?.let { context ->
entryInfo?.username?.let { userName -> // Set copy buttons
assignUserName(userName) { templateView.apply {
mClipboardHelper?.timeoutCopyToClipboard(userName, setOnAskCopySafeClickListener {
getString(R.string.copy_field, showClipboardDialog()
getString(R.string.entry_user_name)))
}
} }
val showWarningClipboardDialogOnClickListener = View.OnClickListener { setOnCopyActionClickListener { field ->
showClipboardDialog(entryInfo) mClipboardHelper?.timeoutCopyToClipboard(
} field.protectedValue.stringValue,
val onPasswordCopyClickListener: View.OnClickListener? = if (mAllowCopyPasswordAndProtectedFields) { getString(
View.OnClickListener { R.string.copy_field,
entryInfo?.password?.let { password -> TemplateField.getLocalizedName(context, field.name)
mClipboardHelper?.timeoutCopyToClipboard(password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
}
} else {
// If dialog not already shown
if (mIsFirstTimeAskAllowCopyPasswordAndProtectedFields) {
showWarningClipboardDialogOnClickListener
} else {
null
}
}
assignPassword(entryInfo?.password,
mAllowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener)
//Assign OTP field
entryInfo?.otpModel?.let { otpModel ->
val otpElement = OtpElement(otpModel)
assignOtp(otpElement) {
mClipboardHelper?.timeoutCopyToClipboard(
otpElement.token,
getString(R.string.copy_field, getString(R.string.entry_otp))
) )
} )
} }
assignURL(entryInfo?.url)
assignNotes(entryInfo?.notes)
// Assign custom fields
if (mDatabase?.allowEntryCustomFields() == true) {
clearExtraFields()
entryInfo?.customFields?.forEach { field ->
val label = field.name
// OTP field is already managed in dedicated view
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || mAllowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
addExtraField(label, value, allowCopyProtectedField) {
mClipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field,
TemplateField.getLocalizedName(context, field.name))
)
}
} else {
// If dialog not already shown
if (mIsFirstTimeAskAllowCopyPasswordAndProtectedFields) {
addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
} else {
addExtraField(label, value, allowCopyProtectedField, null)
}
}
}
}
}
setHiddenProtectedValue(mHideProtectedValue)
// Manage attachments
entryInfo?.attachments?.toSet()?.let { attachments ->
assignAttachments(attachments)
}
// Assign dates
assignCreationDate(entryInfo?.creationTime)
assignModificationDate(entryInfo?.lastModificationTime)
setExpires(entryInfo?.expires ?: false, entryInfo?.expiryTime)
// Assign special data
assignUUID(entryInfo?.id)
} }
// Populate entry views
templateView.setEntryInfo(entryInfo)
//Assign OTP field
assignOtp(entryInfo)
// Manage attachments
entryInfo?.attachments?.toSet()?.let { attachments ->
assignAttachments(attachments)
}
// Assign dates
assignCreationDate(entryInfo?.creationTime)
assignModificationDate(entryInfo?.lastModificationTime)
// Assign special data
assignUUID(entryInfo?.id)
} }
private fun showClipboardDialog(entryInfo: EntryInfo?) { private fun showClipboardDialog() {
context?.let { context?.let {
AlertDialog.Builder(it) AlertDialog.Builder(it)
.setMessage( .setMessage(
@@ -260,114 +169,53 @@ class EntryFragment: DatabaseFragment() {
.create().apply { .create().apply {
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ -> setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true) PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
dialog.dismiss() finishDialog(dialog)
assignEntryInfo(entryInfo)
} }
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ -> setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false) PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
dialog.dismiss() finishDialog(dialog)
assignEntryInfo(entryInfo)
} }
show() show()
} }
} }
} }
private fun assignUserName(userName: String?, private fun finishDialog(dialog: DialogInterface) {
onClickListener: View.OnClickListener?) { dialog.dismiss()
userNameFieldView.apply { loadTemplateSettings()
if (userName != null && userName.isNotEmpty()) { templateView.reload()
visibility = View.VISIBLE
setValue(userName)
applyFontVisibility(mFontInVisibility)
showOrHideEntryFieldsContainer(false)
} else {
visibility = View.GONE
}
assignCopyButtonClickListener(onClickListener)
}
} }
private fun assignPassword(password: String?, private fun assignOtp(entryInfo: EntryInfo?) {
allowCopyPassword: Boolean, entryInfo?.otpModel?.let { otpModel ->
onClickListener: View.OnClickListener?) { val otpElement = OtpElement(otpModel)
passwordFieldView.apply { templateView.getOtpTokenView()?.let { otpFieldView ->
if (password != null && password.isNotEmpty()) { otpFieldView.removeCallbacks(mOtpRunnable)
visibility = View.VISIBLE if (otpElement.token.isEmpty()) {
setValue(password, true) otpFieldView.setLabel(R.string.entry_otp)
applyFontVisibility(mFontInVisibility) otpFieldView.setValue(R.string.error_invalid_OTP)
activateCopyButton(allowCopyPassword) otpFieldView.setCopyButtonState(EntryFieldView.ButtonState.GONE)
showOrHideEntryFieldsContainer(false) } else {
} else { otpFieldView.label = otpElement.type.name
visibility = View.GONE otpFieldView.value = otpElement.token
} otpFieldView.setCopyButtonState(EntryFieldView.ButtonState.ACTIVATE)
assignCopyButtonClickListener(onClickListener) otpFieldView.setCopyButtonClickListener {
} mClipboardHelper?.timeoutCopyToClipboard(
} otpElement.token,
getString(R.string.copy_field, getString(R.string.entry_otp))
private fun assignOtp(otpElement: OtpElement?, )
onClickListener: View.OnClickListener) { }
otpFieldView.removeCallbacks(mOtpRunnable) mOtpRunnable = Runnable {
if (otpElement.shouldRefreshToken()) {
if (otpElement != null) { otpFieldView.value = otpElement.token
otpFieldView.visibility = View.VISIBLE }
mEntryViewModel.onOtpElementUpdated(otpElement)
if (otpElement.token.isEmpty()) { otpFieldView.postDelayed(mOtpRunnable, 1000)
otpFieldView.setValue(R.string.error_invalid_OTP)
otpFieldView.activateCopyButton(false)
otpFieldView.assignCopyButtonClickListener(null)
} else {
otpFieldView.setLabel(otpElement.type.name)
otpFieldView.setValue(otpElement.token)
otpFieldView.assignCopyButtonClickListener(onClickListener)
mOtpRunnable = Runnable {
if (otpElement.shouldRefreshToken()) {
otpFieldView.setValue(otpElement.token)
} }
mEntryViewModel.onOtpElementUpdated(otpElement) mEntryViewModel.onOtpElementUpdated(otpElement)
otpFieldView.postDelayed(mOtpRunnable, 1000) otpFieldView.post(mOtpRunnable)
} }
mEntryViewModel.onOtpElementUpdated(otpElement)
otpFieldView.post(mOtpRunnable)
} }
showOrHideEntryFieldsContainer(false)
} else {
otpFieldView.visibility = View.GONE
}
}
private fun assignURL(url: String?) {
urlFieldView.apply {
if (url != null && url.isNotEmpty()) {
visibility = View.VISIBLE
setValue(url)
showOrHideEntryFieldsContainer(false)
} else {
visibility = View.GONE
}
}
}
private fun assignNotes(notes: String?) {
notesFieldView.apply {
if (notes != null && notes.isNotEmpty()) {
visibility = View.VISIBLE
setValue(notes)
applyFontVisibility(mFontInVisibility)
showOrHideEntryFieldsContainer(false)
} else {
visibility = View.GONE
}
}
}
private fun setExpires(isExpires: Boolean, expiryTime: DateInstant?) {
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
expiresDateView.text = if (isExpires) {
expiryTime?.getDateTimeString(resources)
} else {
resources.getString(R.string.never)
} }
} }
@@ -384,54 +232,6 @@ class EntryFragment: DatabaseFragment() {
uuidReferenceView.text = UuidUtil.toHexString(uuid) uuidReferenceView.text = UuidUtil.toHexString(uuid)
} }
private fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
// Hidden style for custom fields
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryFieldView)
childCustomView.hiddenProtectedValue = hiddenProtectedValue
}
}
}
private fun showOrHideEntryFieldsContainer(hide: Boolean) {
entryFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
}
/* -------------
* Extra Fields
* -------------
*/
private fun showOrHideExtraFieldsContainer(hide: Boolean) {
extraFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
}
private fun addExtraField(title: String,
value: ProtectedString,
allowCopy: Boolean,
onCopyButtonClickListener: View.OnClickListener?) {
context?.let { context ->
extraFieldsListView.addView(EntryFieldView(context).apply {
setLabel(TemplateField.getLocalizedName(context, title))
setValue(value.toString(), value.isProtected)
setAutoLink()
activateCopyButton(allowCopy)
assignCopyButtonClickListener(onCopyButtonClickListener)
applyFontVisibility(mFontInVisibility)
})
showOrHideExtraFieldsContainer(false)
}
}
private fun clearExtraFields() {
extraFieldsListView.removeAllViews()
showOrHideExtraFieldsContainer(true)
}
/* ------------- /* -------------
* Attachments * Attachments
* ------------- * -------------
@@ -460,14 +260,7 @@ class EntryFragment: DatabaseFragment() {
fun firstEntryFieldCopyView(): View? { fun firstEntryFieldCopyView(): View? {
return try { return try {
when { templateView.getActionImageView()
userNameFieldView.isVisible && userNameFieldView.copyButtonView.isVisible -> userNameFieldView.copyButtonView
passwordFieldView.isVisible && passwordFieldView.copyButtonView.isVisible -> passwordFieldView.copyButtonView
otpFieldView.isVisible && otpFieldView.copyButtonView.isVisible -> otpFieldView.copyButtonView
urlFieldView.isVisible && urlFieldView.copyButtonView.isVisible -> urlFieldView.copyButtonView
notesFieldView.isVisible && notesFieldView.copyButtonView.isVisible -> notesFieldView.copyButtonView
else -> null
}
} catch (e: Exception) { } catch (e: Exception) {
null null
} }

View File

@@ -226,12 +226,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val containsUsernameToCopy = entry.username.isNotEmpty() val containsUsernameToCopy = entry.username.isNotEmpty()
val containsPasswordToCopy = entry.password.isNotEmpty() val containsPasswordToCopy = entry.password.isNotEmpty()
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(context) && PreferencesUtil.allowCopyProtectedFields(context)
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD) val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
val containsExtraFieldToCopy = entry.customFields.isNotEmpty() val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
&& (entry.containsCustomFieldsNotProtected() && (entry.containsCustomFieldsNotProtected()
|| ||
(entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) (entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyProtectedFields(context))
) )
var startService = false var startService = false
@@ -277,7 +277,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
entry.customFields.forEach { field -> entry.customFields.forEach { field ->
//If value is not protected or allowed //If value is not protected or allowed
if ((!field.protectedValue.isProtected if ((!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) || PreferencesUtil.allowCopyProtectedFields(context))
&& field.name != OTP_TOKEN_FIELD) { && field.name != OTP_TOKEN_FIELD) {
notificationFields.add( notificationFields.add(
ClipboardEntryNotificationField( ClipboardEntryNotificationField(

View File

@@ -352,13 +352,13 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.monospace_font_fields_enable_default)) context.resources.getBoolean(R.bool.monospace_font_fields_enable_default))
} }
fun isFirstTimeAskAllowCopyPasswordAndProtectedFields(context: Context): Boolean { fun isFirstTimeAskAllowCopyProtectedFields(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.allow_copy_password_first_time_key), return prefs.getBoolean(context.getString(R.string.allow_copy_password_first_time_key),
context.resources.getBoolean(R.bool.allow_copy_password_first_time_default)) context.resources.getBoolean(R.bool.allow_copy_password_first_time_default))
} }
fun allowCopyPasswordAndProtectedFields(context: Context): Boolean { fun allowCopyProtectedFields(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.allow_copy_password_key), return prefs.getBoolean(context.getString(R.string.allow_copy_password_key),
context.resources.getBoolean(R.bool.allow_copy_password_default)) context.resources.getBoolean(R.bool.allow_copy_password_default))

View File

@@ -0,0 +1,3 @@
package com.kunzisoft.keepass.view
data class DataDate(val year: Int, val month: Int, val day: Int)

View File

@@ -0,0 +1,3 @@
package com.kunzisoft.keepass.view
data class DataTime(val hours: Int, val minutes: Int)

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2021 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.util.AttributeSet
import android.view.LayoutInflater
import android.widget.CompoundButton
import android.widget.FrameLayout
import android.widget.TextView
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant
class DateTimeEditView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var entryExpiresLabelView: TextInputLayout
private var entryExpiresTextView: TextView
private var entryExpiresCheckBox: CompoundButton
private var mDateTime: DateInstant = DateInstant.IN_ONE_MONTH_DATE_TIME
var setOnDateClickListener: ((DateInstant) -> Unit)? = null
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_edit_date_time, this)
entryExpiresLabelView = findViewById(R.id.expiration_label)
entryExpiresTextView = findViewById(R.id.expiration_text)
entryExpiresCheckBox = findViewById(R.id.expiration_checkbox)
entryExpiresTextView.setOnClickListener {
if (entryExpiresCheckBox.isChecked)
setOnDateClickListener?.invoke(dateTime)
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
}
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
mDateTime.getDateTimeString(resources)
} else {
resources.getString(R.string.never)
}
}
var label: String
get() {
return entryExpiresLabelView.hint.toString()
}
set(value) {
entryExpiresLabelView.hint = value
}
var activation: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
if (!value) {
mDateTime = when (mDateTime.type) {
DateInstant.Type.DATE_TIME -> DateInstant.IN_ONE_MONTH_DATE_TIME
DateInstant.Type.DATE -> DateInstant.IN_ONE_MONTH_DATE
DateInstant.Type.TIME -> DateInstant.IN_ONE_HOUR_TIME
}
}
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var dateTime: DateInstant
get() {
return if (activation)
mDateTime
else
DateInstant.NEVER_EXPIRES
}
set(value) {
mDateTime = value
assignExpiresDateText()
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Jeremy Jamet / Kunzisoft. * Copyright 2021 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -22,69 +22,58 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.CompoundButton import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.settings.PreferencesUtil import java.util.*
class DateTimeView @JvmOverloads constructor(context: Context, class DateTimeView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
: ConstraintLayout(context, attrs, defStyle) { : FrameLayout(context, attrs, defStyle) {
private var entryExpiresLabelView: TextInputLayout private var dateTimeLabelView: TextView
private var entryExpiresTextView: TextView private var dateTimeValueView: TextView
private var entryExpiresCheckBox: CompoundButton private var expiresImage: ImageView
private var mActivated: Boolean = false
private var mDateTime: DateInstant = DateInstant.IN_ONE_MONTH_DATE_TIME private var mDateTime: DateInstant = DateInstant.IN_ONE_MONTH_DATE_TIME
private var fontInVisibility: Boolean = false
var setOnDateClickListener: ((DateInstant) -> Unit)? = null var setOnDateClickListener: ((DateInstant) -> Unit)? = null
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_expiration, this) inflater?.inflate(R.layout.view_date_time, this)
entryExpiresLabelView = findViewById(R.id.expiration_label) dateTimeLabelView = findViewById(R.id.date_time_label)
entryExpiresTextView = findViewById(R.id.expiration_text) dateTimeValueView = findViewById(R.id.date_time_value)
entryExpiresCheckBox = findViewById(R.id.expiration_checkbox) expiresImage = findViewById(R.id.expires_image)
entryExpiresTextView.setOnClickListener {
if (entryExpiresCheckBox.isChecked)
setOnDateClickListener?.invoke(dateTime)
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(context)
} }
private fun assignExpiresDateText() { private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) { dateTimeValueView.text = if (mActivated) {
expiresImage.isVisible = mDateTime.date.before(Date())
mDateTime.getDateTimeString(resources) mDateTime.getDateTimeString(resources)
} else { } else {
expiresImage.isVisible = false
resources.getString(R.string.never) resources.getString(R.string.never)
} }
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
} }
var label: String var label: String
get() { get() {
return entryExpiresLabelView.hint.toString() return dateTimeLabelView.text.toString()
} }
set(value) { set(value) {
entryExpiresLabelView.hint = value dateTimeLabelView.text = value
} }
var activation: Boolean var activation: Boolean
get() { get() {
return entryExpiresCheckBox.isChecked return mActivated
} }
set(value) { set(value) {
if (!value) { if (!value) {
@@ -94,7 +83,7 @@ class DateTimeView @JvmOverloads constructor(context: Context,
DateInstant.Type.TIME -> DateInstant.IN_ONE_HOUR_TIME DateInstant.Type.TIME -> DateInstant.IN_ONE_HOUR_TIME
} }
} }
entryExpiresCheckBox.isChecked = value mActivated = value
assignExpiresDateText() assignExpiresDateText()
} }

View File

@@ -2,7 +2,6 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.text.InputType import android.text.InputType
import android.text.method.PasswordTransformationMethod
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
@@ -144,13 +143,10 @@ class EntryEditFieldView @JvmOverloads constructor(context: Context,
} }
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean) { fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean) {
// hiddenProtectedValue don't work with TextInputLayout
if (protection) { 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
labelView.editText?.transformationMethod = if (hiddenProtectedValue)
PasswordTransformationMethod.getInstance()
else
null
} }
} }
@@ -163,7 +159,4 @@ class EntryEditFieldView @JvmOverloads constructor(context: Context,
actionImageButton.visibility = if (onActionClickListener == null) View.GONE else View.VISIBLE actionImageButton.visibility = if (onActionClickListener == null) View.GONE else View.VISIBLE
} }
enum class TextType {
NORMAL, SMALL_MULTI_LINE, MULTI_LINE
}
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.view package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.text.InputType
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -29,10 +30,12 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.text.util.LinkifyCompat import androidx.core.text.util.LinkifyCompat
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
class EntryFieldView @JvmOverloads constructor(context: Context, class EntryFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
@@ -41,17 +44,7 @@ class EntryFieldView @JvmOverloads constructor(context: Context,
private val labelView: TextView private val labelView: TextView
private val valueView: TextView private val valueView: TextView
private val showButtonView: ImageView private val showButtonView: ImageView
val copyButtonView: ImageView private val copyButtonView: ImageView
private var isProtected = false
var hiddenProtectedValue: Boolean
get() {
return !showButtonView.isSelected
}
set(value) {
showButtonView.isSelected = value
changeProtectedValueParameters()
}
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
@@ -69,19 +62,58 @@ class EntryFieldView @JvmOverloads constructor(context: Context,
valueView.applyFontVisibility() valueView.applyFontVisibility()
} }
fun setLabel(label: String?) { var label: String
labelView.text = label ?: "" get() {
} return labelView.text.toString()
}
set(value) {
labelView.text = value
}
fun setLabel(@StringRes labelId: Int) { fun setLabel(@StringRes labelId: Int) {
labelView.setText(labelId) labelView.setText(labelId)
} }
fun setValue(value: String?, var value: String
isProtected: Boolean = false) { get() {
valueView.text = value ?: "" return valueView.text.toString()
this.isProtected = isProtected }
showButtonView.visibility = if (isProtected) View.VISIBLE else View.GONE set(value) {
valueView.text = value
changeProtectedValueParameters()
}
fun setValue(@StringRes valueId: Int) {
value = resources.getString(valueId)
changeProtectedValueParameters()
}
fun setType(valueType: TextType) {
valueView.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
when (valueType) {
TextType.NORMAL -> {
valueView.inputType = valueView.inputType or
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_NORMAL
valueView.maxLines = 1
}
TextType.SMALL_MULTI_LINE -> {
valueView.inputType = valueView.inputType or
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
valueView.maxEms = 3
valueView.maxLines = 3
}
TextType.MULTI_LINE -> {
valueView.inputType = valueView.inputType or
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
valueView.maxEms = 40
valueView.maxLines = 40
}
}
}
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) {
showButtonView.isVisible = protection
showButtonView.isSelected = hiddenProtectedValue
showButtonView.setOnClickListener { showButtonView.setOnClickListener {
showButtonView.isSelected = !showButtonView.isSelected showButtonView.isSelected = !showButtonView.isSelected
changeProtectedValueParameters() changeProtectedValueParameters()
@@ -89,28 +121,20 @@ class EntryFieldView @JvmOverloads constructor(context: Context,
changeProtectedValueParameters() changeProtectedValueParameters()
} }
fun setValue(@StringRes valueId: Int,
isProtected: Boolean = false) {
setValue(resources.getString(valueId), isProtected)
}
private fun changeProtectedValueParameters() { private fun changeProtectedValueParameters() {
valueView.apply { valueView.apply {
if (isProtected) { if (showButtonView.isVisible) {
isFocusable = false isFocusable = false
setTextIsSelectable(false)
applyHiddenStyle(showButtonView.isSelected)
} else { } else {
isFocusable = true
setTextIsSelectable(true) setTextIsSelectable(true)
linkify()
} }
applyHiddenStyle(isProtected && showButtonView.isSelected)
if (!isProtected) linkify()
} }
} }
fun setAutoLink() {
if (!isProtected) linkify()
changeProtectedValueParameters()
}
private fun linkify() { private fun linkify() {
when { when {
labelView.text.contains(APPLICATION_ID_FIELD_NAME) -> { labelView.text.contains(APPLICATION_ID_FIELD_NAME) -> {
@@ -127,17 +151,37 @@ class EntryFieldView @JvmOverloads constructor(context: Context,
} }
} }
fun setLinkAll() { fun getCopyButtonView(): View? {
LinkifyCompat.addLinks(valueView, Linkify.ALL) if (copyButtonView.isVisible) {
return copyButtonView
}
return null
} }
fun activateCopyButton(enable: Boolean) { fun setCopyButtonState(buttonState: ButtonState) {
// Reverse because isActivated show custom color and allow click when (buttonState) {
copyButtonView.isActivated = !enable ButtonState.ACTIVATE -> {
copyButtonView.visibility = VISIBLE
copyButtonView.isActivated = false
}
ButtonState.DEACTIVATE -> {
copyButtonView.visibility = VISIBLE
// Reverse because isActivated show custom color and allow click
copyButtonView.isActivated = true
}
ButtonState.GONE -> {
copyButtonView.visibility = GONE
copyButtonView.setOnClickListener(null)
}
}
} }
fun assignCopyButtonClickListener(onClickActionListener: OnClickListener?) { fun setCopyButtonClickListener(onClickActionListener: OnClickListener?) {
copyButtonView.setOnClickListener(onClickActionListener) copyButtonView.setOnClickListener(onClickActionListener)
copyButtonView.visibility = if (onClickActionListener == null) GONE else VISIBLE copyButtonView.isVisible = onClickActionListener != null
}
enum class ButtonState {
ACTIVATE, DEACTIVATE, GONE
} }
} }

View File

@@ -0,0 +1,432 @@
package com.kunzisoft.keepass.view
import android.app.Activity
import android.content.Context
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.IdRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.*
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
abstract class TemplateAbstractView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var mTemplate: Template? = null
protected var mEntryInfo: EntryInfo? = null
protected var mCustomFieldIds = mutableListOf<FieldId>()
protected var mFontInVisibility: Boolean = false
protected var mHideProtectedValue: Boolean = false
protected var headerContainerView: ViewGroup
protected var entryIconView: ImageView
protected var templateContainerView: ViewGroup
protected var customFieldsContainerView: SectionView
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_template, this)
headerContainerView = findViewById(R.id.entry_edit_header_container)
entryIconView = findViewById(R.id.entry_edit_icon_button)
templateContainerView = findViewById(R.id.template_fields_container)
// To fix card view margin in KitKat-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val paddingVertical = resources.getDimensionPixelSize(R.dimen.card_view_margin_vertical)
val paddingHorizontal = resources.getDimensionPixelSize(R.dimen.card_view_margin_horizontal)
templateContainerView.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
}
customFieldsContainerView = findViewById(R.id.custom_fields_container)
}
// To show icon image
var populateIconMethod: ((ImageView, IconImage) -> Unit)? = null
fun setTemplate(template: Template?) {
if (mTemplate != template) {
mTemplate = template
if (mEntryInfo != null) {
populateEntryInfoWithViews(true)
}
buildTemplateAndPopulateInfo()
clearFocus()
(context.getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager?)
?.hideSoftInputFromWindow(windowToken, 0)
}
}
fun buildTemplate() {
// Retrieve preferences
mHideProtectedValue = PreferencesUtil.hideProtectedValue(context)
// Build each template section
templateContainerView.removeAllViews()
customFieldsContainerView.removeAllViews()
mCustomFieldIds.clear()
mTemplate?.let { template ->
buildHeader()
template.sections.forEach { templateSection ->
val sectionView = SectionView(context, null, R.attr.cardViewStyle)
// Add build view to parent
templateContainerView.addView(sectionView)
// Build each attribute
templateSection.attributes.forEach { templateAttribute ->
val fieldTag: String
when {
templateAttribute.label.equals(TemplateField.LABEL_TITLE, true) -> {
throw Exception("title cannot be in template attribute")
}
templateAttribute.label.equals(TemplateField.LABEL_USERNAME, true) -> {
fieldTag = FIELD_USERNAME_TAG
}
templateAttribute.label.equals(TemplateField.LABEL_PASSWORD, true) -> {
fieldTag = FIELD_PASSWORD_TAG
}
templateAttribute.label.equals(TemplateField.LABEL_URL, true) -> {
fieldTag = FIELD_URL_TAG
}
templateAttribute.label.equals(
TemplateField.LABEL_EXPIRATION,
true
) -> {
fieldTag = FIELD_EXPIRES_TAG
}
templateAttribute.label.equals(TemplateField.LABEL_NOTES, true) -> {
fieldTag = FIELD_NOTES_TAG
}
else -> {
fieldTag = FIELD_CUSTOM_TAG
}
}
val attributeView = buildViewForTemplateField(
templateAttribute,
Field(
templateAttribute.label,
ProtectedString(templateAttribute.protected, "")
),
fieldTag
)
// Add created view to this parent
sectionView.addView(attributeView)
}
}
}
}
abstract fun buildHeader()
private fun buildViewForCustomField(field: Field): View? {
val customFieldTemplateAttribute = TemplateAttribute(
field.name,
TemplateAttributeType.MULTILINE,
field.protectedValue.isProtected,
field.protectedValue.stringValue,
TemplateAttributeAction.CUSTOM_EDITION)
return buildViewForTemplateField(customFieldTemplateAttribute, field, FIELD_CUSTOM_TAG)
}
private fun buildViewForTemplateField(templateAttribute: TemplateAttribute,
field: Field,
fieldTag: String): View? {
// Build main view depending on type
val itemView: View? = when (templateAttribute.type) {
TemplateAttributeType.INLINE,
TemplateAttributeType.SMALL_MULTILINE,
TemplateAttributeType.MULTILINE -> {
buildLinearTextView(templateAttribute, field)
}
TemplateAttributeType.DATE,
TemplateAttributeType.TIME,
TemplateAttributeType.DATETIME -> {
buildDataTimeView(templateAttribute, field)
}
}
// Custom id defined by field name, use getViewByField(field: Field) to retrieve it
itemView?.id = field.name.hashCode()
itemView?.tag = fieldTag
// Add new custom view id to the custom field list
if (fieldTag == FIELD_CUSTOM_TAG) {
val indexOldItem = indexCustomFieldIdByName(field.name)
if (indexOldItem >= 0)
mCustomFieldIds.removeAt(indexOldItem)
mCustomFieldIds.add(FieldId(field.name, itemView!!.id, field.protectedValue.isProtected))
}
return itemView
}
protected abstract fun buildLinearTextView(templateAttribute: TemplateAttribute,
field: Field): View?
protected abstract fun buildDataTimeView(templateAttribute: TemplateAttribute,
field: Field): View?
abstract fun getActionImageView(): View?
fun setFontInVisibility(fontInVisibility: Boolean) {
this.mFontInVisibility = fontInVisibility
}
fun setHideProtectedValue(hideProtectedValue: Boolean) {
this.mHideProtectedValue = hideProtectedValue
}
fun setEntryInfo(entryInfo: EntryInfo?) {
mEntryInfo = entryInfo
buildTemplateAndPopulateInfo()
}
protected abstract fun populateViewsWithEntryInfo()
fun getEntryInfo(): EntryInfo {
populateEntryInfoWithViews(true)
return mEntryInfo ?: EntryInfo()
}
abstract fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean)
fun reload() {
buildTemplateAndPopulateInfo()
}
private fun buildTemplateAndPopulateInfo() {
if (mTemplate != null && mEntryInfo != null) {
buildTemplate()
populateViewsWithEntryInfo()
}
}
/* -------------
* External value update
* -------------
*/
protected fun getFieldViewById(@IdRes viewId: Int): View? {
return templateContainerView.findViewById(viewId)
?: customFieldsContainerView.findViewById(viewId)
}
/* -------------
* Custom Fields
* -------------
*/
protected data class FieldId(var label: String, var viewId: Int, var protected: Boolean)
private fun isStandardFieldName(name: String): Boolean {
return TemplateField.isStandardFieldName(name)
}
protected fun customFieldIdByName(name: String): FieldId? {
return mCustomFieldIds.find { it.label.equals(name, true) }
}
protected fun indexCustomFieldIdByName(name: String): Int {
return mCustomFieldIds.indexOfFirst { it.label.equals(name, true) }
}
protected fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false) {
mEntryInfo?.customFields = mCustomFieldIds.mapNotNull {
getCustomField(it.label, templateFieldNotEmpty)
}.toMutableList()
}
protected fun getCustomField(fieldName: String): Field {
return getCustomField(fieldName, false)
?: Field(fieldName, ProtectedString(false, ""))
}
protected abstract fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field?
/**
* Update a custom field or create a new one if doesn't exists, the old value is lost
*/
protected fun putCustomField(customField: Field, focus: Boolean): Boolean {
return if (!isStandardFieldName(customField.name)) {
customFieldsContainerView.visibility = View.VISIBLE
if (indexCustomFieldIdByName(customField.name) >= 0) {
replaceCustomField(customField, customField, focus)
} else {
val newCustomView = buildViewForCustomField(customField)
customFieldsContainerView.addView(newCustomView)
val fieldId = FieldId(customField.name,
newCustomView!!.id,
customField.protectedValue.isProtected)
val indexOldItem = indexCustomFieldIdByName(fieldId.label)
if (indexOldItem >= 0)
mCustomFieldIds.removeAt(indexOldItem)
mCustomFieldIds.add(indexOldItem, fieldId)
if (focus)
newCustomView.requestFocus()
true
}
} else {
false
}
}
fun putCustomField(customField: Field): Boolean {
val put = putCustomField(customField, true)
retrieveCustomFieldsFromView()
return put
}
/**
* Update a custom field and keep the old value
*/
private fun replaceCustomField(oldField: Field, newField: Field, focus: Boolean): Boolean {
if (!isStandardFieldName(newField.name)) {
customFieldIdByName(oldField.name)?.viewId?.let { viewId ->
customFieldsContainerView.findViewById<View>(viewId)?.let { viewToReplace ->
val oldValue = getCustomField(oldField.name).protectedValue.toString()
val parentGroup = viewToReplace.parent as ViewGroup
val indexInParent = parentGroup.indexOfChild(viewToReplace)
parentGroup.removeView(viewToReplace)
val newCustomFieldWithValue = Field(newField.name,
ProtectedString(newField.protectedValue.isProtected, oldValue))
val oldPosition = indexCustomFieldIdByName(oldField.name)
if (oldPosition >= 0)
mCustomFieldIds.removeAt(oldPosition)
val newCustomView = buildViewForCustomField(newCustomFieldWithValue)
parentGroup.addView(newCustomView, indexInParent)
mCustomFieldIds.add(oldPosition, FieldId(newCustomFieldWithValue.name,
newCustomView!!.id,
newCustomFieldWithValue.protectedValue.isProtected))
if (focus)
newCustomView.requestFocus()
return true
}
}
}
return false
}
fun replaceCustomField(oldField: Field, newField: Field): Boolean {
val replace = replaceCustomField(oldField, newField, true)
retrieveCustomFieldsFromView()
return replace
}
fun removeCustomField(oldCustomField: Field) {
val indexOldField = indexCustomFieldIdByName(oldCustomField.name)
if (indexOldField >= 0) {
mCustomFieldIds[indexOldField].viewId.let { viewId ->
customFieldsContainerView.removeViewById(viewId)
}
mCustomFieldIds.removeAt(indexOldField)
}
retrieveCustomFieldsFromView()
}
fun putOtpElement(otpElement: OtpElement) {
val otpField = OtpEntryFields.buildOtpField(otpElement,
mEntryInfo?.title, mEntryInfo?.username)
putCustomField(Field(otpField.name, otpField.protectedValue))
}
override fun onRestoreInstanceState(state: Parcelable?) {
//begin boilerplate code so parent classes can restore state
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
} else {
mTemplate = state.template
mEntryInfo = state.entryInfo
onRestoreEntryInstanceState(state)
buildTemplateAndPopulateInfo()
super.onRestoreInstanceState(state.superState)
}
}
protected open fun onRestoreEntryInstanceState(state: SavedState) {}
override fun onSaveInstanceState(): Parcelable {
val superSave = super.onSaveInstanceState()
val saveState = SavedState(superSave)
populateEntryInfoWithViews(false)
saveState.template = this.mTemplate
saveState.entryInfo = this.mEntryInfo
onSaveEntryInstanceState(saveState)
return saveState
}
protected open fun onSaveEntryInstanceState(savedState: SavedState) {}
protected class SavedState : BaseSavedState {
var template: Template? = null
var entryInfo: EntryInfo? = null
// TODO Move
var tempDateTimeViewId: Int? = null
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
template = parcel.readParcelable(Template::class.java.classLoader)
?: template
entryInfo = parcel.readParcelable(EntryInfo::class.java.classLoader)
?: entryInfo
val dateTimeViewId = parcel.readInt()
if (dateTimeViewId != -1)
tempDateTimeViewId = dateTimeViewId
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeParcelable(template, flags)
out.writeParcelable(entryInfo, flags)
out.writeInt(tempDateTimeViewId ?: -1)
}
companion object {
//required field that makes Parcelables from a Parcel
@JvmField val CREATOR = object : Creator<SavedState?> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}
companion object {
const val FIELD_TITLE_TAG = "FIELD_TITLE_TAG"
const val FIELD_USERNAME_TAG = "FIELD_USERNAME_TAG"
const val FIELD_PASSWORD_TAG = "FIELD_PASSWORD_TAG"
const val FIELD_URL_TAG = "FIELD_URL_TAG"
const val FIELD_EXPIRES_TAG = "FIELD_EXPIRES_TAG"
const val FIELD_NOTES_TAG = "FIELD_NOTES_TAG"
const val FIELD_CUSTOM_TAG = "FIELD_CUSTOM_TAG"
}
}

View File

@@ -0,0 +1,332 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.View
import androidx.annotation.IdRes
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
import com.kunzisoft.keepass.database.element.template.TemplateAttributeType
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.element.template.TemplateField.LABEL_TITLE
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import org.joda.time.DateTime
class TemplateEditView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: TemplateAbstractView(context, attrs, defStyle) {
// Current date time selection
@IdRes
private var mTempDateTimeViewId: Int? = null
private var mOnCustomEditionActionClickListener: ((Field) -> Unit)? = null
fun setOnCustomEditionActionClickListener(listener: ((Field) -> Unit)?) {
this.mOnCustomEditionActionClickListener = listener
}
private var mOnPasswordGenerationActionClickListener: ((Field) -> Unit)? = null
fun setOnPasswordGenerationActionClickListener(listener: ((Field) -> Unit)?) {
this.mOnPasswordGenerationActionClickListener = listener
}
private var mOnDateInstantClickListener: ((DateInstant) -> Unit)? = null
fun setOnDateInstantClickListener(listener: ((DateInstant) -> Unit)?) {
this.mOnDateInstantClickListener = listener
}
fun setOnIconClickListener(onClickListener: OnClickListener) {
entryIconView.setOnClickListener(onClickListener)
}
fun getIcon(): IconImage {
return mEntryInfo?.icon ?: IconImage()
}
fun setIcon(iconImage: IconImage) {
mEntryInfo?.icon = iconImage
populateIconMethod?.invoke(entryIconView, iconImage)
}
override fun buildHeader() {
headerContainerView.isVisible = true
findViewById<EntryEditFieldView?>(R.id.entry_edit_title)?.apply {
tag = FIELD_TITLE_TAG
id = LABEL_TITLE.hashCode()
label = TemplateField.getLocalizedName(context, LABEL_TITLE)
}
}
override fun buildLinearTextView(templateAttribute: TemplateAttribute,
field: Field): View? {
// Add an action icon if needed
return context?.let {
EntryEditFieldView(it).apply {
applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
label = TemplateField.getLocalizedName(context, field.name)
setType(when (templateAttribute.type) {
TemplateAttributeType.SMALL_MULTILINE -> TextType.SMALL_MULTI_LINE
TemplateAttributeType.MULTILINE -> TextType.MULTI_LINE
else -> TextType.NORMAL
})
value = field.protectedValue.stringValue
when (templateAttribute.action) {
TemplateAttributeAction.NONE -> {
setOnActionClickListener(null)
}
TemplateAttributeAction.CUSTOM_EDITION -> {
setOnActionClickListener({
mOnCustomEditionActionClickListener?.invoke(field)
}, R.drawable.ic_more_white_24dp)
}
TemplateAttributeAction.PASSWORD_GENERATION -> {
setOnActionClickListener({
mOnPasswordGenerationActionClickListener?.invoke(field)
}, R.drawable.ic_generate_password_white_24dp)
}
}
templateAttribute.options.forEach { option ->
// TODO options
}
}
}
}
override fun buildDataTimeView(templateAttribute: TemplateAttribute,
field: Field): View? {
return context?.let {
DateTimeEditView(it).apply {
label = TemplateField.getLocalizedName(context, field.name)
try {
val value = field.protectedValue.toString()
activation = value.trim().isNotEmpty()
dateTime = DateInstant(value,
when (templateAttribute.type) {
TemplateAttributeType.DATE -> DateInstant.Type.DATE
TemplateAttributeType.TIME -> DateInstant.Type.TIME
else -> DateInstant.Type.DATE_TIME
})
} catch (e: Exception) {
activation = false
dateTime = when (templateAttribute.type) {
TemplateAttributeType.DATE -> DateInstant.IN_ONE_MONTH_DATE
TemplateAttributeType.TIME -> DateInstant.IN_ONE_HOUR_TIME
else -> DateInstant.IN_ONE_MONTH_DATE_TIME
}
}
setOnDateClickListener = { dateInstant ->
mTempDateTimeViewId = id
mOnDateInstantClickListener?.invoke(dateInstant)
}
}
}
}
override fun getActionImageView(): View? {
return findViewWithTag<EntryEditFieldView?>(FIELD_PASSWORD_TAG)?.getActionImageView()
}
fun setPasswordField(passwordField: Field) {
val passwordView = getFieldViewById(passwordField.name.hashCode())
if (passwordView is EntryEditFieldView?) {
passwordView?.value = passwordField.protectedValue.stringValue
}
}
fun getPasswordField(): Field {
val passwordView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG)
return Field(TemplateField.LABEL_PASSWORD, ProtectedString(true, passwordView?.value ?: ""))
}
private fun setCurrentDateTimeSelection(action: (dateInstant: DateInstant) -> DateInstant) {
mTempDateTimeViewId?.let { viewId ->
val dateTimeView = getFieldViewById(viewId)
if (dateTimeView is DateTimeEditView) {
dateTimeView.dateTime = DateInstant(
action.invoke(dateTimeView.dateTime).date,
dateTimeView.dateTime.type)
}
}
}
fun setCurrentDateTimeValue(date: DataDate) {
// Save the date
setCurrentDateTimeSelection { instant ->
val newDateInstant = DateInstant(
DateTime(instant.date)
.withYear(date.year)
.withMonthOfYear(date.month + 1)
.withDayOfMonth(date.day)
.toDate(), instant.type)
if (instant.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(instant.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mOnDateInstantClickListener?.invoke(instantTime)
}
newDateInstant
}
}
fun setCurrentTimeValue(time: DataTime) {
setCurrentDateTimeSelection { instant ->
DateInstant(
DateTime(instant.date)
.withHourOfDay(time.hours)
.withMinuteOfHour(time.minutes)
.toDate(), instant.type)
}
}
override fun populateViewsWithEntryInfo() {
mEntryInfo?.let { entryInfo ->
setIcon(entryInfo.icon)
val titleView: EntryEditFieldView? =
findViewWithTag(FIELD_TITLE_TAG)
titleView?.value = entryInfo.title
titleView?.applyFontVisibility(mFontInVisibility)
val userNameView: EntryEditFieldView? =
templateContainerView.findViewWithTag(FIELD_USERNAME_TAG)
userNameView?.value = entryInfo.username
userNameView?.applyFontVisibility(mFontInVisibility)
val passwordView: EntryEditFieldView? =
templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG)
passwordView?.value = entryInfo.password
passwordView?.applyFontVisibility(mFontInVisibility)
val urlView: EntryEditFieldView? = templateContainerView.findViewWithTag(
FIELD_URL_TAG
)
urlView?.value = entryInfo.url
urlView?.applyFontVisibility(mFontInVisibility)
val expirationView: DateTimeEditView? =
templateContainerView.findViewWithTag(FIELD_EXPIRES_TAG)
expirationView?.activation = entryInfo.expires
expirationView?.dateTime = entryInfo.expiryTime
val notesView: EntryEditFieldView? =
templateContainerView.findViewWithTag(FIELD_NOTES_TAG)
notesView?.value = entryInfo.notes
notesView?.applyFontVisibility(mFontInVisibility)
customFieldsContainerView.removeAllViews()
entryInfo.customFields.forEach { customField ->
val indexFieldViewId = indexCustomFieldIdByName(customField.name)
if (indexFieldViewId >= 0) {
// Template contains the custom view
val customFieldId = mCustomFieldIds[indexFieldViewId]
templateContainerView.findViewById<View>(customFieldId.viewId)
?.let { customView ->
if (customView is EntryEditFieldView) {
customView.value = customField.protectedValue.stringValue
customView.applyFontVisibility(mFontInVisibility)
} else if (customView is DateTimeEditView) {
try {
customView.dateTime =
DateInstant(customField.protectedValue.stringValue)
} catch (e: Exception) {
Log.e(TAG, "unable to populate date time view", e)
}
}
}
} else {
// If template view not found, create a new custom view
putCustomField(customField, false)
}
}
}
}
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) {
if (mEntryInfo == null)
mEntryInfo = EntryInfo()
// Icon already populate
val titleView: EntryEditFieldView? = findViewWithTag(FIELD_TITLE_TAG)
titleView?.value?.let {
mEntryInfo?.title = it
}
val userNameView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_USERNAME_TAG)
userNameView?.value?.let {
mEntryInfo?.username = it
}
val passwordView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG)
passwordView?.value?.let {
mEntryInfo?.password = it
}
val urlView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_URL_TAG)
urlView?.value?.let {
mEntryInfo?.url = it
}
val expirationView: DateTimeEditView? = templateContainerView.findViewWithTag(FIELD_EXPIRES_TAG)
expirationView?.activation?.let {
mEntryInfo?.expires = it
}
expirationView?.dateTime?.let {
mEntryInfo?.expiryTime = it
}
val notesView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_NOTES_TAG)
notesView?.value?.let {
mEntryInfo?.notes = it
}
retrieveCustomFieldsFromView(templateFieldNotEmpty)
mEntryInfo?.otpModel = OtpEntryFields.parseFields { key ->
getCustomField(key).protectedValue.toString()
}?.otpModel
}
override fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? {
customFieldIdByName(fieldName)?.let { fieldId ->
val editView: View? = templateContainerView.findViewById(fieldId.viewId)
?: customFieldsContainerView.findViewById(fieldId.viewId)
if (editView is EntryEditFieldView) {
if (!templateFieldNotEmpty ||
(editView.tag == FIELD_CUSTOM_TAG
&& editView.value.isNotEmpty()))
return Field(fieldName, ProtectedString(fieldId.protected, editView.value))
}
if (editView is DateTimeEditView) {
val value = if (editView.activation) editView.dateTime.toString() else ""
if (!templateFieldNotEmpty ||
(editView.tag == FIELD_CUSTOM_TAG
&& value.isNotEmpty()))
return Field(fieldName, ProtectedString(fieldId.protected, value))
}
}
return null
}
override fun onRestoreEntryInstanceState(state: SavedState) {
mTempDateTimeViewId = state.tempDateTimeViewId
}
override fun onSaveEntryInstanceState(savedState: SavedState) {
savedState.tempDateTimeViewId = this.mTempDateTimeViewId
}
companion object {
private val TAG = TemplateEditView::class.java.name
}
}

View File

@@ -1,380 +1,188 @@
package com.kunzisoft.keepass.view package com.kunzisoft.keepass.view
import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import androidx.core.view.isVisible
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.IdRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.* import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateAttributeType
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import org.joda.time.DateTime
class TemplateView @JvmOverloads constructor(context: Context, class TemplateView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) { : TemplateAbstractView(context, attrs, defStyle) {
private var mTemplate: Template? = null private var mOnAskCopySafeClickListener: (() -> Unit)? = null
private var mEntryInfo: EntryInfo? = null fun setOnAskCopySafeClickListener(listener: (() -> Unit)? = null) {
this.mOnAskCopySafeClickListener = listener
private var mCustomFieldIds = mutableListOf<FieldId>() }
private var mOnCopyActionClickListener: ((Field) -> Unit)? = null
private var mHideProtectedValue: Boolean = false fun setOnCopyActionClickListener(listener: ((Field) -> Unit)? = null) {
private var mFontInVisibility: Boolean = false this.mOnCopyActionClickListener = listener
private var entryIconView: ImageView
private var templateContainerView: ViewGroup
private var customFieldsContainerView: SectionView
// Current date time selection
@IdRes
private var mTempDateTimeViewId: Int? = null
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_template, this)
entryIconView = findViewById(R.id.entry_edit_icon_button)
templateContainerView = findViewById(R.id.template_fields_container)
// To fix card view margin in KitKat-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val paddingVertical = resources.getDimensionPixelSize(R.dimen.card_view_margin_vertical)
val paddingHorizontal = resources.getDimensionPixelSize(R.dimen.card_view_margin_horizontal)
templateContainerView.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
}
customFieldsContainerView = findViewById(R.id.custom_fields_container)
} }
fun setOnIconClickListener(onClickListener: OnClickListener) { private var mFirstTimeAskAllowCopyProtectedFields: Boolean = false
entryIconView.setOnClickListener(onClickListener) fun setFirstTimeAskAllowCopyProtectedFields(firstTimeAskAllowCopyProtectedFields : Boolean) {
this.mFirstTimeAskAllowCopyProtectedFields = firstTimeAskAllowCopyProtectedFields
} }
private var mOnCustomEditionActionClickListener: ((Field) -> Unit)? = null private var mAllowCopyProtectedFields: Boolean = false
fun setOnCustomEditionActionClickListener(listener: ((Field) -> Unit)?) { fun setAllowCopyProtectedFields(allowCopyProtectedFields : Boolean) {
this.mOnCustomEditionActionClickListener = listener this.mAllowCopyProtectedFields = allowCopyProtectedFields
} }
private var mOnPasswordGenerationActionClickListener: ((Field) -> Unit)? = null override fun buildHeader() {
fun setOnPasswordGenerationActionClickListener(listener: ((Field) -> Unit)?) { headerContainerView.isVisible = false
this.mOnPasswordGenerationActionClickListener = listener
} }
private var mOnDateInstantClickListener: ((DateInstant) -> Unit)? = null override fun buildLinearTextView(templateAttribute: TemplateAttribute,
fun setOnDateInstantClickListener(listener: ((DateInstant) -> Unit)?) { field: Field): View? {
this.mOnDateInstantClickListener = listener
}
// To show icon image
var populateIconMethod: ((ImageView, IconImage) -> Unit)? = null
fun setTemplate(template: Template?) {
if (mTemplate != template) {
mTemplate = template
if (mEntryInfo != null) {
populateEntryInfoWithViews()
}
buildTemplateAndPopulateInfo()
clearFocus()
(context.getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager?)
?.hideSoftInputFromWindow(windowToken, 0)
}
}
fun buildTemplate() {
// Retrieve preferences
mHideProtectedValue = PreferencesUtil.hideProtectedValue(context)
// Build each template section
templateContainerView.removeAllViews()
customFieldsContainerView.removeAllViews()
mCustomFieldIds.clear()
mTemplate?.let { template ->
findViewById<EntryEditFieldView?>(R.id.entry_edit_title)?.apply {
tag = FIELD_TITLE_TAG
id = TemplateField.LABEL_TITLE.hashCode()
label = TemplateField.getLocalizedName(context, TemplateField.LABEL_TITLE)
}
template.sections.forEach { templateSection ->
val sectionView = SectionView(context, null, R.attr.cardViewStyle)
// Add build view to parent
templateContainerView.addView(sectionView)
// Build each attribute
templateSection.attributes.forEach { templateAttribute ->
val fieldTag: String
when {
templateAttribute.label.equals(TemplateField.LABEL_TITLE, true) -> {
throw Exception("title cannot be in template attribute")
}
templateAttribute.label.equals(TemplateField.LABEL_USERNAME, true) -> {
fieldTag = FIELD_USERNAME_TAG
}
templateAttribute.label.equals(TemplateField.LABEL_PASSWORD, true) -> {
fieldTag = FIELD_PASSWORD_TAG
}
templateAttribute.label.equals(TemplateField.LABEL_URL, true) -> {
fieldTag = FIELD_URL_TAG
}
templateAttribute.label.equals(
TemplateField.LABEL_EXPIRATION,
true
) -> {
fieldTag = FIELD_EXPIRES_TAG
}
templateAttribute.label.equals(TemplateField.LABEL_NOTES, true) -> {
fieldTag = FIELD_NOTES_TAG
}
else -> {
fieldTag = FIELD_CUSTOM_TAG
}
}
val attributeView = buildViewForTemplateField(
templateAttribute,
Field(
templateAttribute.label,
ProtectedString(templateAttribute.protected, "")
),
fieldTag
)
// Add created view to this parent
sectionView.addView(attributeView)
}
}
}
}
private fun buildViewForCustomField(field: Field): View? {
val customFieldTemplateAttribute = TemplateAttribute(
field.name,
TemplateAttributeType.MULTILINE,
field.protectedValue.isProtected,
field.protectedValue.stringValue,
TemplateAttributeAction.CUSTOM_EDITION)
return buildViewForTemplateField(customFieldTemplateAttribute, field, FIELD_CUSTOM_TAG)
}
private fun buildViewForTemplateField(templateAttribute: TemplateAttribute,
field: Field,
fieldTag: String): View? {
// Build main view depending on type
val itemView: View? = when (templateAttribute.type) {
TemplateAttributeType.INLINE,
TemplateAttributeType.SMALL_MULTILINE,
TemplateAttributeType.MULTILINE -> {
buildLinearTextView(templateAttribute, field)
}
TemplateAttributeType.DATE,
TemplateAttributeType.TIME,
TemplateAttributeType.DATETIME -> {
buildDataTimeView(templateAttribute, field)
}
}
// Custom id defined by field name, use getViewByField(field: Field) to retrieve it
itemView?.id = field.name.hashCode()
itemView?.tag = fieldTag
// Add new custom view id to the custom field list
if (fieldTag == FIELD_CUSTOM_TAG) {
val indexOldItem = indexCustomFieldIdByName(field.name)
if (indexOldItem >= 0)
mCustomFieldIds.removeAt(indexOldItem)
mCustomFieldIds.add(FieldId(field.name, itemView!!.id, field.protectedValue.isProtected))
}
return itemView
}
private fun buildLinearTextView(templateAttribute: TemplateAttribute,
field: Field): View? {
// Add an action icon if needed // Add an action icon if needed
return context?.let { return context?.let {
EntryEditFieldView(it).apply { EntryFieldView(it).apply {
label = TemplateField.getLocalizedName(context, field.name) applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue) setProtection(field.protectedValue.isProtected, mHideProtectedValue)
label = TemplateField.getLocalizedName(context, field.name)
setType(when (templateAttribute.type) { setType(when (templateAttribute.type) {
TemplateAttributeType.SMALL_MULTILINE -> EntryEditFieldView.TextType.SMALL_MULTI_LINE TemplateAttributeType.SMALL_MULTILINE -> TextType.SMALL_MULTI_LINE
TemplateAttributeType.MULTILINE -> EntryEditFieldView.TextType.MULTI_LINE TemplateAttributeType.MULTILINE -> TextType.MULTI_LINE
else -> EntryEditFieldView.TextType.NORMAL else -> TextType.NORMAL
}) })
value = field.protectedValue.stringValue value = field.protectedValue.stringValue
when (templateAttribute.action) {
TemplateAttributeAction.NONE -> { if (field.protectedValue.isProtected) {
setOnActionClickListener(null) if (mFirstTimeAskAllowCopyProtectedFields) {
setCopyButtonState(EntryFieldView.ButtonState.DEACTIVATE)
setCopyButtonClickListener {
mOnAskCopySafeClickListener?.invoke()
}
} else {
if (mAllowCopyProtectedFields) {
setCopyButtonState(EntryFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener {
mOnCopyActionClickListener?.invoke(field)
}
} else {
setCopyButtonState(EntryFieldView.ButtonState.GONE)
setCopyButtonClickListener(null)
}
} }
TemplateAttributeAction.CUSTOM_EDITION -> { } else {
setOnActionClickListener({ setCopyButtonState(EntryFieldView.ButtonState.ACTIVATE)
mOnCustomEditionActionClickListener?.invoke(field) setCopyButtonClickListener {
}, R.drawable.ic_more_white_24dp) mOnCopyActionClickListener?.invoke(field)
}
TemplateAttributeAction.PASSWORD_GENERATION -> {
setOnActionClickListener({
mOnPasswordGenerationActionClickListener?.invoke(field)
}, R.drawable.ic_generate_password_white_24dp)
} }
} }
templateAttribute.options.forEach { option -> templateAttribute.options.forEach { option ->
// TODO options // TODO options
} }
applyFontVisibility(mFontInVisibility)
} }
} }
} }
private fun buildDataTimeView(templateAttribute: TemplateAttribute, override fun buildDataTimeView(templateAttribute: TemplateAttribute,
field: Field): View? { field: Field): View? {
return context?.let { return context?.let {
DateTimeView(it).apply { DateTimeView(it).apply {
label = TemplateField.getLocalizedName(context, field.name) label = TemplateField.getLocalizedName(context, field.name)
try { dateTime = try {
val value = field.protectedValue.toString() val value = field.protectedValue.toString()
activation = value.trim().isNotEmpty() activation = value.trim().isNotEmpty()
dateTime = DateInstant(value, DateInstant(value,
when (templateAttribute.type) { when (templateAttribute.type) {
TemplateAttributeType.DATE -> DateInstant.Type.DATE TemplateAttributeType.DATE -> DateInstant.Type.DATE
TemplateAttributeType.TIME -> DateInstant.Type.TIME TemplateAttributeType.TIME -> DateInstant.Type.TIME
else -> DateInstant.Type.DATE_TIME else -> DateInstant.Type.DATE_TIME
}) })
} catch (e: Exception) { } catch (e: Exception) {
activation = false activation = false
dateTime = when (templateAttribute.type) { when (templateAttribute.type) {
TemplateAttributeType.DATE -> DateInstant.IN_ONE_MONTH_DATE TemplateAttributeType.DATE -> DateInstant.IN_ONE_MONTH_DATE
TemplateAttributeType.TIME -> DateInstant.IN_ONE_HOUR_TIME TemplateAttributeType.TIME -> DateInstant.IN_ONE_HOUR_TIME
else -> DateInstant.IN_ONE_MONTH_DATE_TIME else -> DateInstant.IN_ONE_MONTH_DATE_TIME
} }
} }
setOnDateClickListener = { dateInstant ->
mTempDateTimeViewId = id
mOnDateInstantClickListener?.invoke(dateInstant)
}
} }
} }
} }
fun getActionImageView(): View? { override fun getActionImageView(): View? {
return findViewWithTag<EntryEditFieldView?>(FIELD_PASSWORD_TAG)?.getActionImageView() return findViewWithTag<EntryFieldView?>(FIELD_PASSWORD_TAG)?.getCopyButtonView()
} }
fun setFontInVisibility(fontInVisibility: Boolean) { override fun populateViewsWithEntryInfo() {
this.mFontInVisibility = fontInVisibility
}
fun getIcon(): IconImage {
return mEntryInfo?.icon ?: IconImage()
}
fun setIcon(iconImage: IconImage) {
mEntryInfo?.icon = iconImage
populateIconMethod?.invoke(entryIconView, iconImage)
}
fun setPasswordField(passwordField: Field) {
val passwordView = getFieldViewById(passwordField.name.hashCode())
if (passwordView is EntryEditFieldView?) {
passwordView?.value = passwordField.protectedValue.stringValue
}
}
fun getPasswordField(): Field {
val passwordView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG)
return Field(TemplateField.LABEL_PASSWORD, ProtectedString(true, passwordView?.value ?: ""))
}
fun setCurrentDateTimeValue(date: Date) {
// Save the date
setCurrentDateTimeSelection { instant ->
val newDateInstant = DateInstant(DateTime(instant.date)
.withYear(date.year)
.withMonthOfYear(date.month + 1)
.withDayOfMonth(date.day)
.toDate(), instant.type)
if (instant.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(instant.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mOnDateInstantClickListener?.invoke(instantTime)
}
newDateInstant
}
}
fun setCurrentTimeValue(time: Time) {
setCurrentDateTimeSelection { instant ->
DateInstant(DateTime(instant.date)
.withHourOfDay(time.hours)
.withMinuteOfHour(time.minutes)
.toDate(), instant.type)
}
}
fun setEntryInfo(entryInfo: EntryInfo?) {
mEntryInfo = entryInfo
buildTemplateAndPopulateInfo()
}
private fun populateViewsWithEntryInfo() {
mEntryInfo?.let { entryInfo -> mEntryInfo?.let { entryInfo ->
setIcon(entryInfo.icon)
val titleView: EntryEditFieldView? = val titleView: EntryFieldView? =
findViewWithTag(FIELD_TITLE_TAG) findViewWithTag(FIELD_TITLE_TAG)
titleView?.value = entryInfo.title titleView?.value = entryInfo.title
titleView?.applyFontVisibility(mFontInVisibility) titleView?.applyFontVisibility(mFontInVisibility)
if (entryInfo.title.isEmpty()) {
titleView?.isVisible = false
}
val userNameView: EntryEditFieldView? = val userNameView: EntryFieldView? =
templateContainerView.findViewWithTag(FIELD_USERNAME_TAG) templateContainerView.findViewWithTag(FIELD_USERNAME_TAG)
userNameView?.value = entryInfo.username userNameView?.value = entryInfo.username
userNameView?.applyFontVisibility(mFontInVisibility) userNameView?.applyFontVisibility(mFontInVisibility)
if (entryInfo.username.isEmpty()) {
userNameView?.isVisible = false
}
val passwordView: EntryEditFieldView? = val passwordView: EntryFieldView? =
templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG) templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG)
passwordView?.value = entryInfo.password passwordView?.value = entryInfo.password
passwordView?.applyFontVisibility(mFontInVisibility) passwordView?.applyFontVisibility(mFontInVisibility)
if (entryInfo.password.isEmpty()) {
passwordView?.isVisible = false
}
val urlView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_URL_TAG) val urlView: EntryFieldView? = templateContainerView.findViewWithTag(
FIELD_URL_TAG
)
urlView?.value = entryInfo.url urlView?.value = entryInfo.url
urlView?.applyFontVisibility(mFontInVisibility) urlView?.applyFontVisibility(mFontInVisibility)
if (entryInfo.url.isEmpty()) {
urlView?.isVisible = false
}
val expirationView: DateTimeView? = val expirationView: DateTimeView? =
templateContainerView.findViewWithTag(FIELD_EXPIRES_TAG) templateContainerView.findViewWithTag(FIELD_EXPIRES_TAG)
expirationView?.activation = entryInfo.expires expirationView?.activation = entryInfo.expires
expirationView?.dateTime = entryInfo.expiryTime expirationView?.dateTime = entryInfo.expiryTime
if (!entryInfo.expires) {
expirationView?.isVisible = false
}
val notesView: EntryEditFieldView? = val notesView: EntryFieldView? =
templateContainerView.findViewWithTag(FIELD_NOTES_TAG) templateContainerView.findViewWithTag(FIELD_NOTES_TAG)
notesView?.value = entryInfo.notes notesView?.value = entryInfo.notes
notesView?.applyFontVisibility(mFontInVisibility) notesView?.applyFontVisibility(mFontInVisibility)
if (entryInfo.notes.isEmpty()) {
notesView?.isVisible = false
}
customFieldsContainerView.removeAllViews() customFieldsContainerView.removeAllViews()
val emptyCustomFields = mutableListOf<FieldId>().also { it.addAll(mCustomFieldIds) }
entryInfo.customFields.forEach { customField -> entryInfo.customFields.forEach { customField ->
val indexFieldViewId = indexCustomFieldIdByName(customField.name) val indexFieldViewId = indexCustomFieldIdByName(customField.name)
if (indexFieldViewId >= 0) { if (indexFieldViewId >= 0) {
// Template contains the custom view // Template contains the custom view
val customFieldId = mCustomFieldIds[indexFieldViewId] val customFieldId = mCustomFieldIds[indexFieldViewId]
emptyCustomFields.remove(customFieldId)
templateContainerView.findViewById<View>(customFieldId.viewId) templateContainerView.findViewById<View>(customFieldId.viewId)
?.let { customView -> ?.let { customView ->
if (customView is EntryEditFieldView) { if (customView is EntryFieldView) {
customView.value = customField.protectedValue.stringValue customView.value = customField.protectedValue.stringValue
customView.applyFontVisibility(mFontInVisibility) customView.applyFontVisibility(mFontInVisibility)
} else if (customView is DateTimeView) { } else if (customView is DateTimeView) {
@@ -391,36 +199,37 @@ class TemplateView @JvmOverloads constructor(context: Context,
putCustomField(customField, false) putCustomField(customField, false)
} }
} }
// Hide empty custom fields
emptyCustomFields.forEach { customFieldId ->
templateContainerView.findViewById<View>(customFieldId.viewId)
.isVisible = false
}
} }
} }
fun getEntryInfo(): EntryInfo { override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) {
populateEntryInfoWithViews()
return mEntryInfo ?: EntryInfo()
}
fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean = true) {
if (mEntryInfo == null) if (mEntryInfo == null)
mEntryInfo = EntryInfo() mEntryInfo = EntryInfo()
// Icon already populate // Icon already populate
val titleView: EntryEditFieldView? = findViewWithTag(FIELD_TITLE_TAG) val titleView: EntryFieldView? = findViewWithTag(FIELD_TITLE_TAG)
titleView?.value?.let { titleView?.value?.let {
mEntryInfo?.title = it mEntryInfo?.title = it
} }
val userNameView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_USERNAME_TAG) val userNameView: EntryFieldView? = templateContainerView.findViewWithTag(FIELD_USERNAME_TAG)
userNameView?.value?.let { userNameView?.value?.let {
mEntryInfo?.username = it mEntryInfo?.username = it
} }
val passwordView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG) val passwordView: EntryFieldView? = templateContainerView.findViewWithTag(FIELD_PASSWORD_TAG)
passwordView?.value?.let { passwordView?.value?.let {
mEntryInfo?.password = it mEntryInfo?.password = it
} }
val urlView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_URL_TAG) val urlView: EntryFieldView? = templateContainerView.findViewWithTag(FIELD_URL_TAG)
urlView?.value?.let { urlView?.value?.let {
mEntryInfo?.url = it mEntryInfo?.url = it
} }
@@ -433,7 +242,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
mEntryInfo?.expiryTime = it mEntryInfo?.expiryTime = it
} }
val notesView: EntryEditFieldView? = templateContainerView.findViewWithTag(FIELD_NOTES_TAG) val notesView: EntryFieldView? = templateContainerView.findViewWithTag(FIELD_NOTES_TAG)
notesView?.value?.let { notesView?.value?.let {
mEntryInfo?.notes = it mEntryInfo?.notes = it
} }
@@ -445,248 +254,38 @@ class TemplateView @JvmOverloads constructor(context: Context,
}?.otpModel }?.otpModel
} }
private fun buildTemplateAndPopulateInfo() { fun getOtpTokenView(): EntryFieldView? {
if (mTemplate != null && mEntryInfo != null) { val indexFieldViewId = indexCustomFieldIdByName(OTP_TOKEN_FIELD)
buildTemplate() if (indexFieldViewId >= 0) {
populateViewsWithEntryInfo() // Template contains the custom view
val customFieldId = mCustomFieldIds[indexFieldViewId]
return findViewById(customFieldId.viewId)
} }
return null
} }
/* ------------- override fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? {
* External value update
* -------------
*/
private fun getFieldViewById(@IdRes viewId: Int): View? {
return templateContainerView.findViewById(viewId)
?: customFieldsContainerView.findViewById(viewId)
}
private fun setCurrentDateTimeSelection(action: (dateInstant: DateInstant) -> DateInstant) {
mTempDateTimeViewId?.let { viewId ->
val dateTimeView = getFieldViewById(viewId)
if (dateTimeView is DateTimeView) {
dateTimeView.dateTime = DateInstant(
action.invoke(dateTimeView.dateTime).date,
dateTimeView.dateTime.type)
}
}
}
/* -------------
* Custom Fields
* -------------
*/
private data class FieldId(var label: String, var viewId: Int, var protected: Boolean)
private fun isStandardFieldName(name: String): Boolean {
return TemplateField.isStandardFieldName(name)
}
private fun customFieldIdByName(name: String): FieldId? {
return mCustomFieldIds.find { it.label.equals(name, true) }
}
private fun indexCustomFieldIdByName(name: String): Int {
return mCustomFieldIds.indexOfFirst { it.label.equals(name, true) }
}
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false) {
mEntryInfo?.customFields = mCustomFieldIds.mapNotNull {
getCustomField(it.label, templateFieldNotEmpty)
}.toMutableList()
}
private fun getCustomField(fieldName: String): Field {
return getCustomField(fieldName, false)
?: Field(fieldName, ProtectedString(false, ""))
}
private fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? {
customFieldIdByName(fieldName)?.let { fieldId -> customFieldIdByName(fieldName)?.let { fieldId ->
val editView: View? = templateContainerView.findViewById(fieldId.viewId) val editView: View? = templateContainerView.findViewById(fieldId.viewId)
?: customFieldsContainerView.findViewById(fieldId.viewId) ?: customFieldsContainerView.findViewById(fieldId.viewId)
if (editView is EntryEditFieldView) { if (editView is EntryFieldView) {
if (!templateFieldNotEmpty || if (!templateFieldNotEmpty ||
(editView.tag == FIELD_CUSTOM_TAG (editView.tag == FIELD_CUSTOM_TAG
&& editView.value.isNotEmpty())) && editView.value.isNotEmpty()))
return Field(fieldName, ProtectedString(fieldId.protected, editView.value)) return Field(fieldName, ProtectedString(fieldId.protected, editView.value))
} }
if (editView is DateTimeView) { if (editView is DateTimeView) {
val value = if (editView.activation) editView.dateTime.toString() else "" val value = if (editView.activation) editView.dateTime.toString() else ""
if (!templateFieldNotEmpty || if (!templateFieldNotEmpty ||
(editView.tag == FIELD_CUSTOM_TAG (editView.tag == FIELD_CUSTOM_TAG
&& value.isNotEmpty())) && value.isNotEmpty()))
return Field(fieldName, ProtectedString(fieldId.protected, value)) return Field(fieldName, ProtectedString(fieldId.protected, value))
} }
} }
return null return null
} }
/**
* Update a custom field or create a new one if doesn't exists, the old value is lost
*/
private fun putCustomField(customField: Field, focus: Boolean): Boolean {
return if (!isStandardFieldName(customField.name)) {
customFieldsContainerView.visibility = View.VISIBLE
if (indexCustomFieldIdByName(customField.name) >= 0) {
replaceCustomField(customField, customField, focus)
} else {
val newCustomView = buildViewForCustomField(customField)
customFieldsContainerView.addView(newCustomView)
val fieldId = FieldId(customField.name,
newCustomView!!.id,
customField.protectedValue.isProtected)
val indexOldItem = indexCustomFieldIdByName(fieldId.label)
if (indexOldItem >= 0)
mCustomFieldIds.removeAt(indexOldItem)
mCustomFieldIds.add(indexOldItem, fieldId)
if (focus)
newCustomView.requestFocus()
true
}
} else {
false
}
}
fun putCustomField(customField: Field): Boolean {
val put = putCustomField(customField, true)
retrieveCustomFieldsFromView()
return put
}
/**
* Update a custom field and keep the old value
*/
private fun replaceCustomField(oldField: Field, newField: Field, focus: Boolean): Boolean {
if (!isStandardFieldName(newField.name)) {
customFieldIdByName(oldField.name)?.viewId?.let { viewId ->
customFieldsContainerView.findViewById<View>(viewId)?.let { viewToReplace ->
val oldValue = getCustomField(oldField.name).protectedValue.toString()
val parentGroup = viewToReplace.parent as ViewGroup
val indexInParent = parentGroup.indexOfChild(viewToReplace)
parentGroup.removeView(viewToReplace)
val newCustomFieldWithValue = Field(newField.name,
ProtectedString(newField.protectedValue.isProtected, oldValue))
val oldPosition = indexCustomFieldIdByName(oldField.name)
if (oldPosition >= 0)
mCustomFieldIds.removeAt(oldPosition)
val newCustomView = buildViewForCustomField(newCustomFieldWithValue)
parentGroup.addView(newCustomView, indexInParent)
mCustomFieldIds.add(oldPosition, FieldId(newCustomFieldWithValue.name,
newCustomView!!.id,
newCustomFieldWithValue.protectedValue.isProtected))
if (focus)
newCustomView.requestFocus()
return true
}
}
}
return false
}
fun replaceCustomField(oldField: Field, newField: Field): Boolean {
val replace = replaceCustomField(oldField, newField, true)
retrieveCustomFieldsFromView()
return replace
}
fun removeCustomField(oldCustomField: Field) {
val indexOldField = indexCustomFieldIdByName(oldCustomField.name)
if (indexOldField >= 0) {
mCustomFieldIds[indexOldField].viewId.let { viewId ->
customFieldsContainerView.removeViewById(viewId)
}
mCustomFieldIds.removeAt(indexOldField)
}
retrieveCustomFieldsFromView()
}
fun putOtpElement(otpElement: OtpElement) {
val otpField = OtpEntryFields.buildOtpField(otpElement,
mEntryInfo?.title, mEntryInfo?.username)
putCustomField(Field(otpField.name, otpField.protectedValue))
}
override fun onRestoreInstanceState(state: Parcelable?) {
//begin boilerplate code so parent classes can restore state
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
} else {
mTemplate = state.template
mEntryInfo = state.entryInfo
mTempDateTimeViewId = state.tempDateTimeViewId
buildTemplateAndPopulateInfo()
super.onRestoreInstanceState(state.superState)
}
}
override fun onSaveInstanceState(): Parcelable {
val superSave = super.onSaveInstanceState()
val saveState = SavedState(superSave)
populateEntryInfoWithViews(false)
saveState.template = this.mTemplate
saveState.entryInfo = this.mEntryInfo
saveState.tempDateTimeViewId = this.mTempDateTimeViewId
return saveState
}
data class Date(val year: Int, val month: Int, val day: Int)
data class Time(val hours: Int, val minutes: Int)
internal class SavedState : BaseSavedState {
var template: Template? = null
var entryInfo: EntryInfo? = null
var tempDateTimeViewId: Int? = null
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
template = parcel.readParcelable(Template::class.java.classLoader)
?: template
entryInfo = parcel.readParcelable(EntryInfo::class.java.classLoader)
?: entryInfo
val dateTimeViewId = parcel.readInt()
if (dateTimeViewId != -1)
tempDateTimeViewId = dateTimeViewId
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeParcelable(template, flags)
out.writeParcelable(entryInfo, flags)
out.writeInt(tempDateTimeViewId ?: -1)
}
companion object {
//required field that makes Parcelables from a Parcel
@JvmField val CREATOR = object : Creator<SavedState?> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}
companion object { companion object {
private val TAG = TemplateView::class.java.name private val TAG = TemplateEditView::class.java.name
private const val FIELD_TITLE_TAG = "FIELD_TITLE_TAG"
private const val FIELD_USERNAME_TAG = "FIELD_USERNAME_TAG"
private const val FIELD_PASSWORD_TAG = "FIELD_PASSWORD_TAG"
private const val FIELD_URL_TAG = "FIELD_URL_TAG"
private const val FIELD_EXPIRES_TAG = "FIELD_EXPIRES_TAG"
private const val FIELD_NOTES_TAG = "FIELD_NOTES_TAG"
private const val FIELD_CUSTOM_TAG = "FIELD_CUSTOM_TAG"
} }
} }

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.view
enum class TextType {
NORMAL, SMALL_MULTI_LINE, MULTI_LINE
}

View File

@@ -12,7 +12,8 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.view.TemplateView import com.kunzisoft.keepass.view.DataDate
import com.kunzisoft.keepass.view.DataTime
import java.util.* import java.util.*
@@ -58,10 +59,10 @@ class EntryEditViewModel: ViewModel() {
val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection
private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>() private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>()
val onDateSelected : LiveData<TemplateView.Date> get() = _onDateSelected val onDateSelected : LiveData<DataDate> get() = _onDateSelected
private val _onDateSelected = SingleLiveEvent<TemplateView.Date>() private val _onDateSelected = SingleLiveEvent<DataDate>()
val onTimeSelected : LiveData<TemplateView.Time> get() = _onTimeSelected val onTimeSelected : LiveData<DataTime> get() = _onTimeSelected
private val _onTimeSelected = SingleLiveEvent<TemplateView.Time>() private val _onTimeSelected = SingleLiveEvent<DataTime>()
val requestSetupOtp : LiveData<Void?> get() = _requestSetupOtp val requestSetupOtp : LiveData<Void?> get() = _requestSetupOtp
private val _requestSetupOtp = SingleLiveEvent<Void?>() private val _requestSetupOtp = SingleLiveEvent<Void?>()
@@ -274,11 +275,11 @@ class EntryEditViewModel: ViewModel() {
} }
fun selectDate(year: Int, month: Int, day: Int) { fun selectDate(year: Int, month: Int, day: Int) {
_onDateSelected.value = TemplateView.Date(year, month, day) _onDateSelected.value = DataDate(year, month, day)
} }
fun selectTime(hours: Int, minutes: Int) { fun selectTime(hours: Int, minutes: Int) {
_onTimeSelected.value = TemplateView.Time(hours, minutes) _onTimeSelected.value = DataTime(hours, minutes)
} }
fun setupOtp() { fun setupOtp() {

View File

@@ -8,6 +8,7 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
@@ -18,10 +19,14 @@ class EntryViewModel: ViewModel() {
private val mDatabase: Database? = Database.getInstance() private val mDatabase: Database? = Database.getInstance()
private var mEntryTemplate: Template? = null
private var mEntry: Entry? = null private var mEntry: Entry? = null
private var mLastEntryVersion: Entry? = null private var mLastEntryVersion: Entry? = null
private var mHistoryPosition: Int = -1 private var mHistoryPosition: Int = -1
val template : LiveData<Template> get() = _template
private val _template = MutableLiveData<Template>()
val entryInfo : LiveData<EntryInfo> get() = _entryInfo val entryInfo : LiveData<EntryInfo> get() = _entryInfo
private val _entryInfo = MutableLiveData<EntryInfo>() private val _entryInfo = MutableLiveData<EntryInfo>()
@@ -52,11 +57,27 @@ class EntryViewModel: ViewModel() {
} else { } else {
mLastEntryVersion mLastEntryVersion
} }
mEntryTemplate = mEntry?.let {
mDatabase?.getTemplate(it)
} ?: Template.STANDARD
mHistoryPosition = historyPosition mHistoryPosition = historyPosition
createEntryInfoHistory(mEntry)
// To simplify template field visibility
mEntry?.let { entry ->
mDatabase?.decodeEntryWithTemplateConfiguration(entry)?.let {
// To update current modification time
it.touch(modified = false, touchParents = false)
EntryInfoHistory(
mEntryTemplate ?: Template.STANDARD,
it.getEntryInfo(mDatabase),
it.getHistory()
)
}
}
}, },
{ entryInfoHistory -> { entryInfoHistory ->
if (entryInfoHistory != null) { if (entryInfoHistory != null) {
_template.value = entryInfoHistory.template
_entryInfo.value = entryInfoHistory.entryInfo _entryInfo.value = entryInfoHistory.entryInfo
_entryIsHistory.value = mHistoryPosition != -1 _entryIsHistory.value = mHistoryPosition != -1
_entryHistory.value = entryInfoHistory.entryHistory _entryHistory.value = entryInfoHistory.entryHistory
@@ -71,18 +92,6 @@ class EntryViewModel: ViewModel() {
} }
} }
private fun createEntryInfoHistory(entry: Entry?): EntryInfoHistory? {
if (entry != null) {
// To simplify template field visibility
mDatabase?.decodeEntryWithTemplateConfiguration(entry)?.let {
// To update current modification time
it.touch(modified = false, touchParents = false)
return EntryInfoHistory(it.getEntryInfo(mDatabase), it.getHistory())
}
}
return null
}
// TODO Remove // TODO Remove
fun getEntry(): Entry? { fun getEntry(): Entry? {
return mEntry return mEntry
@@ -116,13 +125,15 @@ class EntryViewModel: ViewModel() {
} }
fun onHistorySelected(item: Entry, position: Int) { fun onHistorySelected(item: Entry, position: Int) {
_historySelected.value = EntryHistory(item.nodeId, item, null, position) _historySelected.value = EntryHistory(item.nodeId, null, item, null, position)
} }
data class EntryInfoHistory(val entryInfo: EntryInfo, data class EntryInfoHistory(val template: Template,
val entryInfo: EntryInfo,
val entryHistory: List<Entry>) val entryHistory: List<Entry>)
// Custom data class to manage entry to retrieve and define is it's an history item (!= -1) // Custom data class to manage entry to retrieve and define is it's an history item (!= -1)
data class EntryHistory(var nodeIdUUID: NodeId<UUID>?, data class EntryHistory(var nodeIdUUID: NodeId<UUID>?,
var template: Template?,
var entry: Entry?, var entry: Entry?,
var lastEntryVersion: Entry?, var lastEntryVersion: Entry?,
var historyPosition: Int = -1) var historyPosition: Int = -1)

View File

@@ -18,7 +18,6 @@
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/entry_table" android:id="@+id/entry_table"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -26,69 +25,10 @@
android:paddingTop="@dimen/card_view_margin_vertical" android:paddingTop="@dimen/card_view_margin_vertical"
android:paddingBottom="@dimen/card_view_margin_vertical"> android:paddingBottom="@dimen/card_view_margin_vertical">
<androidx.cardview.widget.CardView <com.kunzisoft.keepass.view.TemplateView
android:id="@+id/entry_fields_container" android:id="@+id/entry_template"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
style="?attr/cardViewStyle">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical">
<!-- Username -->
<com.kunzisoft.keepass.view.EntryFieldView
android:id="@+id/entry_user_name_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<!-- Password -->
<com.kunzisoft.keepass.view.EntryFieldView
android:id="@+id/entry_password_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<!-- OTP -->
<com.kunzisoft.keepass.view.EntryFieldView
android:id="@+id/entry_otp_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<!-- URL -->
<com.kunzisoft.keepass.view.EntryFieldView
android:id="@+id/entry_url_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
<!-- Notes -->
<com.kunzisoft.keepass.view.EntryFieldView
android:id="@+id/entry_notes_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/extra_fields_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
style="?attr/cardViewStyle">
<LinearLayout
android:id="@+id/extra_fields_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/entry_attachments_container" android:id="@+id/entry_attachments_container"
@@ -127,32 +67,6 @@
android:layout_margin="@dimen/card_view_padding" android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical"> android:orientation="vertical">
<!-- Expires -->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_expires_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_expires"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_expires_image"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_info_white_24dp"
android:contentDescription="@string/content_description_file_information"
android:tint="@color/red"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_expires_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
</LinearLayout>
<!-- Created --> <!-- Created -->
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_created_label" android:id="@+id/entry_created_label"

View File

@@ -25,7 +25,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.kunzisoft.keepass.view.TemplateView <com.kunzisoft.keepass.view.TemplateEditView
android:id="@+id/template_view" android:id="@+id/template_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -65,7 +65,7 @@
android:maxLines="3" android:maxLines="3"
android:hint="@string/entry_notes"/> android:hint="@string/entry_notes"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.kunzisoft.keepass.view.DateTimeView <com.kunzisoft.keepass.view.DateTimeEditView
android:id="@+id/group_edit_expiration" android:id="@+id/group_edit_expiration"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/date_time_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_expires"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/expires_image"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_info_white_24dp"
android:contentDescription="@string/content_description_file_information"
android:tint="@color/red"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/date_time_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
</LinearLayout>
</LinearLayout>

View File

@@ -30,6 +30,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="?attr/cardViewStyle" style="?attr/cardViewStyle"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/template_fields_container"> app:layout_constraintBottom_toTopOf="@+id/template_fields_container">
<LinearLayout <LinearLayout