Files
KeePassDX/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt
2022-01-18 11:32:11 +01:00

745 lines
29 KiB
Kotlin

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.util.Log
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.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.*
import com.kunzisoft.keepass.database.element.template.TemplateEngine.Companion.addTemplateDecorator
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<
TEntryFieldView: GenericTextFieldView,
TEntrySelectFieldView: GenericTextFieldView,
TDateTimeView: GenericDateTimeFieldView> @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var mTemplate: Template? = null
protected var mEntryInfo: EntryInfo? = null
private var mViewFields = mutableListOf<ViewField>()
protected var mFontInVisibility: Boolean = PreferencesUtil.fieldFontIsInVisibility(context)
protected var mHideProtectedValue: Boolean = PreferencesUtil.hideProtectedValue(context)
protected var headerContainerView: ViewGroup
protected var entryIconView: ImageView
protected var backgroundColorView: View
protected var foregroundColorView: View
protected var backgroundColorButton: ImageView
protected var foregroundColorButton: ImageView
private var titleContainerView: ViewGroup
protected var templateContainerView: ViewGroup
private var customFieldsContainerView: SectionView
private var notReferencedFieldsContainerView: SectionView
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_template, this)
headerContainerView = findViewById(R.id.template_header_container)
entryIconView = findViewById(R.id.template_icon_button)
backgroundColorView = findViewById(R.id.template_background_color)
foregroundColorView = findViewById(R.id.template_foreground_color)
backgroundColorButton = findViewById(R.id.template_background_color_button)
foregroundColorButton = findViewById(R.id.template_foreground_color_button)
titleContainerView = findViewById(R.id.template_title_container)
templateContainerView = findViewById(R.id.template_fields_container)
// To fix card view margin below Marshmallow
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
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)
notReferencedFieldsContainerView = findViewById(R.id.not_referenced_fields_container)
}
// To show icon image
var populateIconMethod: ((ImageView, IconImage) -> Unit)? = null
set(value) {
field = value
refreshIcon()
}
fun refreshIcon() {
mEntryInfo?.icon?.let {
populateIconMethod?.invoke(entryIconView, it)
}
}
fun setTemplate(template: Template?) {
if (mTemplate != template) {
mTemplate = template
if (mEntryInfo != null) {
populateEntryInfoWithViews(templateFieldNotEmpty = true,
retrieveDefaultValues = false)
}
buildTemplateAndPopulateInfo()
clearFocus()
(context.getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager?)
?.hideSoftInputFromWindow(windowToken, 0)
}
}
private fun buildTemplate() {
// Retrieve preferences
mHideProtectedValue = PreferencesUtil.hideProtectedValue(context)
// Build each template section
titleContainerView.removeAllViews()
templateContainerView.removeAllViews()
customFieldsContainerView.removeAllViews()
notReferencedFieldsContainerView.removeAllViews()
mViewFields.clear()
mTemplate?.let { template ->
preProcessTemplate()
// Create title view
val titleAttribute = TemplateAttribute(
TemplateField.LABEL_TITLE,
TemplateAttributeType.TEXT).apply {
default = template.title
}
val titleView = buildViewForTemplateField(
titleAttribute,
Field(
titleAttribute.label,
ProtectedString(titleAttribute.protected, "")
),
FIELD_TITLE_TAG
)
titleContainerView.addView(titleView)
// Build each section
template.sections.forEach { templateSection ->
val sectionView = SectionView(context, null, R.attr.cardViewStyle)
// Build each attribute
templateSection.attributes.forEach { templateAttribute ->
if (templateAttribute.label != TemplateField.LABEL_TITLE) {
val fieldTag: String =
getTagFromStandardTemplateAttribute(templateAttribute)
val attributeView = buildViewForTemplateField(
templateAttribute,
Field(
templateAttribute.label,
ProtectedString(templateAttribute.protected,
templateAttribute.default)
),
fieldTag
)
// Add created view to this parent
sectionView.addView(attributeView)
}
}
// Add build view to parent
templateContainerView.addView(sectionView)
}
}
}
private fun getTagFromStandardTemplateAttribute(templateAttribute: TemplateAttribute): String {
val fieldTag: String
when {
templateAttribute.label.equals(TemplateField.LABEL_TITLE, true) -> {
fieldTag = FIELD_TITLE_TAG
}
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
}
}
return fieldTag
}
abstract fun preProcessTemplate()
/**
* Not referenced fields are standard fields with content but not in template
*/
private fun buildViewForNotReferencedField(field: Field, templateAttribute: TemplateAttribute): View? {
val fieldTag: String = getTagFromStandardTemplateAttribute(templateAttribute)
return buildViewForTemplateField(templateAttribute, field, fieldTag)
}
private fun buildViewForCustomField(field: Field): View? {
val customFieldTemplateAttribute = TemplateAttribute(
field.name,
TemplateAttributeType.TEXT,
field.protectedValue.isProtected,
TemplateAttributeOption().apply {
setNumberLines(20)
},
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.TEXT -> {
buildLinearTextView(templateAttribute, field) as View?
}
TemplateAttributeType.LIST -> {
buildListItemsView(templateAttribute, field) as View?
}
TemplateAttributeType.DATETIME -> {
buildDataTimeView(templateAttribute, field) as View?
}
TemplateAttributeType.DIVIDER -> null
}
// 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 = getIndexViewFieldByName(field.name)
if (indexOldItem >= 0)
mViewFields.removeAt(indexOldItem)
if (itemView?.id != null) {
mViewFields.add(
ViewField(
itemView,
field
)
)
}
}
return itemView
}
protected abstract fun buildLinearTextView(templateAttribute: TemplateAttribute,
field: Field): TEntryFieldView?
protected abstract fun buildListItemsView(templateAttribute: TemplateAttribute,
field: Field): TEntrySelectFieldView?
protected abstract fun buildDataTimeView(templateAttribute: TemplateAttribute,
field: Field): TDateTimeView?
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()
}
@Suppress("UNCHECKED_CAST")
private fun populateEntryFieldView(fieldTag: String,
templateAttribute: TemplateAttribute,
entryInfoValue: String,
showEmptyFields: Boolean) {
try {
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
if (!showEmptyFields && entryInfoValue.isEmpty()) {
fieldView?.isFieldVisible = false
} else if (fieldView == null && entryInfoValue.isNotEmpty()) {
// Add new not referenced view if standard field not in template
fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")),
templateAttribute
) as? TEntryFieldView?
fieldView?.let {
addNotReferencedView(it as View)
}
}
fieldView?.value = entryInfoValue
fieldView?.applyFontVisibility(mFontInVisibility)
} catch(e: Exception) {
Log.e(TAG, "Unable to populate entry field view", e)
}
}
@Suppress("UNCHECKED_CAST")
private fun populateDateTimeView(fieldTag: String,
templateAttribute: TemplateAttribute,
expires: Boolean,
expiryTime: DateInstant,
showEmptyFields: Boolean) {
try {
var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
if (!showEmptyFields && !expires) {
fieldView?.isFieldVisible = false
} else if (fieldView == null && expires) {
fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")),
templateAttribute
) as? TDateTimeView?
fieldView?.let {
addNotReferencedView(it as View)
}
}
fieldView?.activation = expires
fieldView?.dateTime = expiryTime
} catch(e: Exception) {
Log.e(TAG, "Unable to populate date time view", e)
}
}
/**
* Return empty custom fields
*/
protected open fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
mEntryInfo?.let { entryInfo ->
populateEntryFieldView(FIELD_TITLE_TAG,
Template.TITLE_ATTRIBUTE,
entryInfo.title,
showEmptyFields)
populateEntryFieldView(FIELD_USERNAME_TAG,
Template.USERNAME_ATTRIBUTE,
entryInfo.username,
showEmptyFields)
populateEntryFieldView(FIELD_PASSWORD_TAG,
Template.PASSWORD_ATTRIBUTE,
entryInfo.password,
showEmptyFields)
populateEntryFieldView(FIELD_URL_TAG,
Template.URL_ATTRIBUTE,
entryInfo.url,
showEmptyFields)
populateDateTimeView(FIELD_EXPIRES_TAG,
Template.EXPIRATION_ATTRIBUTE,
entryInfo.expires,
entryInfo.expiryTime,
showEmptyFields)
populateEntryFieldView(FIELD_NOTES_TAG,
Template.NOTES_ATTRIBUTE,
entryInfo.notes,
showEmptyFields)
customFieldsContainerView.removeAllViews()
val emptyCustomFields = mutableListOf<ViewField>().also { it.addAll(mViewFields) }
entryInfo.customFields.forEach { customField ->
val indexFieldViewId = getIndexViewFieldByName(customField.name)
if (indexFieldViewId >= 0) {
// Template contains the custom view
val viewField = mViewFields[indexFieldViewId]
emptyCustomFields.remove(viewField)
viewField.view.let { customView ->
if (customView is GenericTextFieldView) {
customView.value = customField.protectedValue.stringValue
customView.applyFontVisibility(mFontInVisibility)
} else if (customView is GenericDateTimeFieldView) {
try {
customView.activation = true
customView.dateTime = DateInstant(customField
.protectedValue.stringValue)
} catch (e: Exception) {
customView.activation = false
customView.dateTime = DateInstant.NEVER_EXPIRES
Log.e(TAG, "unable to populate date time view", e)
}
}
}
} else {
// If template view not found, create a new custom view
putCustomField(customField, false)
}
}
return emptyCustomFields
}
return emptyList()
}
protected open fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean) {
if (mEntryInfo == null)
mEntryInfo = EntryInfo()
// Icon already populate
try {
val titleView: TEntryFieldView? = findViewWithTag(FIELD_TITLE_TAG)
titleView?.value?.let {
mEntryInfo?.title = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate title view", e)
}
try {
val userNameView: TEntryFieldView? = findViewWithTag(FIELD_USERNAME_TAG)
userNameView?.value?.let {
mEntryInfo?.username = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate username view", e)
}
try {
val passwordView: TEntryFieldView? = findViewWithTag(FIELD_PASSWORD_TAG)
passwordView?.value?.let {
mEntryInfo?.password = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate password view", e)
}
try {
val urlView: TEntryFieldView? = findViewWithTag(FIELD_URL_TAG)
urlView?.value?.let {
mEntryInfo?.url = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate url view", e)
}
try {
val expirationView: TDateTimeView? = findViewWithTag(FIELD_EXPIRES_TAG)
expirationView?.activation?.let {
mEntryInfo?.expires = it
}
expirationView?.dateTime?.let {
mEntryInfo?.expiryTime = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate expiration view", e)
}
try {
val notesView: TEntryFieldView? = findViewWithTag(FIELD_NOTES_TAG)
notesView?.value?.let {
mEntryInfo?.notes = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate notes view", e)
}
retrieveCustomFieldsFromView(templateFieldNotEmpty, retrieveDefaultValues)
}
fun getEntryInfo(): EntryInfo {
populateEntryInfoWithViews(templateFieldNotEmpty = true,
retrieveDefaultValues = true)
return mEntryInfo ?: EntryInfo()
}
fun reload() {
buildTemplateAndPopulateInfo()
}
private fun buildTemplateAndPopulateInfo() {
if (mTemplate != null && mEntryInfo != null) {
buildTemplate()
populateViewsWithEntryInfo(true)
}
}
/* -------------
* External value update
* -------------
*/
protected fun getFieldViewById(@IdRes viewId: Int): View? {
return titleContainerView.findViewById(viewId)
?: templateContainerView.findViewById(viewId)
?: customFieldsContainerView.findViewById(viewId)
?: notReferencedFieldsContainerView.findViewById(viewId)
}
/* -------------
* Custom Fields
* -------------
*/
protected data class ViewField(var view: View, var field: Field)
private fun isStandardFieldName(name: String): Boolean {
return TemplateField.isStandardFieldName(name)
}
protected fun getViewFieldByName(name: String): ViewField? {
return mViewFields.find { it.field.name.equals(name, true) }
}
private fun getIndexViewFieldByName(name: String): Int {
return mViewFields.indexOfFirst { it.field.name.equals(name, true) }
}
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false,
retrieveDefaultValues: Boolean = false) {
mEntryInfo?.customFields = mViewFields.mapNotNull {
getCustomField(it.field.name, templateFieldNotEmpty, retrieveDefaultValues)
}.toMutableList()
}
protected fun getCustomField(fieldName: String): Field {
return getCustomField(fieldName,
templateFieldNotEmpty = false,
retrieveDefaultValues = false
) ?: Field(fieldName, ProtectedString(false))
}
private fun getCustomField(fieldName: String,
templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean): Field? {
getViewFieldByName(fieldName)?.let { fieldId ->
val editView: View = fieldId.view
if (editView is GenericFieldView) {
// Do not return field with a default value
val defaultViewValue =
if (retrieveDefaultValues || editView.value != editView.default) {
editView.value
} else ""
if (!templateFieldNotEmpty
|| (editView.tag == FIELD_CUSTOM_TAG && defaultViewValue.isNotEmpty())) {
return Field(
fieldName,
ProtectedString(fieldId.field.protectedValue.isProtected, defaultViewValue)
)
}
}
}
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 {
if (mTemplate == TemplateEngine.CREATION
&& !TemplateEngine.containsTemplateDecorator(customField.name)) {
customField.name = addTemplateDecorator(customField.name)
}
return if (!isStandardFieldName(customField.name)) {
customFieldsContainerView.visibility = View.VISIBLE
if (getIndexViewFieldByName(customField.name) >= 0) {
// Update a custom field with a new value,
// new field name must be the same as old field name
replaceCustomField(customField, customField, false, focus)
} else {
val newCustomView = buildViewForCustomField(customField)
newCustomView?.let {
customFieldsContainerView.addView(newCustomView)
val fieldId = ViewField(
newCustomView,
customField
)
val indexOldItem = getIndexViewFieldByName(fieldId.field.name)
if (indexOldItem >= 0)
mViewFields.removeAt(indexOldItem)
mViewFields.add(indexOldItem, fieldId)
if (focus)
newCustomView.requestFocus()
}
true
}
} else {
false
}
}
fun putCustomField(customField: Field): Boolean {
val put = putCustomField(customField, true)
retrieveCustomFieldsFromView()
return put
}
private fun replaceCustomField(oldField: Field,
newField: Field,
keepOldValue: Boolean,
focus: Boolean): Boolean {
if (!isStandardFieldName(newField.name)) {
getViewFieldByName(oldField.name)?.view?.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 = if (keepOldValue)
Field(newField.name,
ProtectedString(newField.protectedValue.isProtected, oldValue)
)
else
newField
val oldPosition = getIndexViewFieldByName(oldField.name)
if (oldPosition >= 0)
mViewFields.removeAt(oldPosition)
val newCustomView = buildViewForCustomField(newCustomFieldWithValue)
newCustomView?.let {
parentGroup.addView(newCustomView, indexInParent)
mViewFields.add(
oldPosition,
ViewField(
newCustomView,
newCustomFieldWithValue
)
)
if (focus)
newCustomView.requestFocus()
}
return true
}
}
return false
}
/**
* Update a custom field and keep the old value
*/
fun replaceCustomField(oldField: Field, newField: Field): Boolean {
val replace = replaceCustomField(oldField, newField, keepOldValue = true, focus = true)
retrieveCustomFieldsFromView()
return replace
}
fun removeCustomField(oldCustomField: Field) {
val indexOldField = getIndexViewFieldByName(oldCustomField.name)
if (indexOldField >= 0) {
mViewFields[indexOldField].let { fieldView ->
customFieldsContainerView.removeViewById(fieldView.view.id)
}
mViewFields.removeAt(indexOldField)
}
retrieveCustomFieldsFromView()
}
private fun addNotReferencedView(view: View) {
notReferencedFieldsContainerView.addView(view)
}
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(templateFieldNotEmpty = false,
retrieveDefaultValues = 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"
private val TAG = TemplateAbstractView::class.java.name
}
}