mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Add template / edit / view
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
3
app/src/main/java/com/kunzisoft/keepass/view/DataDate.kt
Normal file
3
app/src/main/java/com/kunzisoft/keepass/view/DataDate.kt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
|
data class DataDate(val year: Int, val month: Int, val day: Int)
|
||||||
3
app/src/main/java/com/kunzisoft/keepass/view/DataTime.kt
Normal file
3
app/src/main/java/com/kunzisoft/keepass/view/DataTime.kt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
|
data class DataTime(val hours: Int, val minutes: Int)
|
||||||
105
app/src/main/java/com/kunzisoft/keepass/view/DateTimeEditView.kt
Normal file
105
app/src/main/java/com/kunzisoft/keepass/view/DateTimeEditView.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
332
app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
Normal file
332
app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5
app/src/main/java/com/kunzisoft/keepass/view/TextType.kt
Normal file
5
app/src/main/java/com/kunzisoft/keepass/view/TextType.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
|
enum class TextType {
|
||||||
|
NORMAL, SMALL_MULTI_LINE, MULTI_LINE
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
30
app/src/main/res/layout/view_date_time.xml
Normal file
30
app/src/main/res/layout/view_date_time.xml
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user