/* * 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 . * */ package com.kunzisoft.keepass.view import android.content.Context import android.text.InputFilter import android.text.util.Linkify import android.util.AttributeSet import android.util.TypedValue import android.view.ContextThemeWrapper import android.view.View import androidx.annotation.StringRes import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.ContextCompat import androidx.core.text.util.LinkifyCompat import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.kunzisoft.keepass.R import com.kunzisoft.keepass.model.AppOriginEntryField.APPLICATION_ID_FIELD_NAME import com.kunzisoft.keepass.utils.AppUtil.openExternalApp open class TextFieldView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : ProtectedTextFieldView(context, attrs, defStyle) { protected var labelViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId() private var showButtonId = ViewCompat.generateViewId() private var copyButtonId = ViewCompat.generateViewId() protected val labelView = AppCompatTextView(context).apply { setTextAppearance(context, R.style.KeepassDXStyle_TextAppearance_LabelTextStyle) layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT ).also { it.leftMargin = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics ).toInt() it.marginStart = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics ).toInt() } } protected val valueView = AppCompatTextView(context).apply { setTextAppearance(context, R.style.KeepassDXStyle_TextAppearance_TextNode) layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).also { it.topMargin = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics ).toInt() it.leftMargin = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics ).toInt() it.marginStart = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics ).toInt() } setTextIsSelectable(true) } private var showButton = AppCompatImageButton( ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply { layoutParams = LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_visibility_state)) contentDescription = context.getString(R.string.menu_showpass) } private var copyButton = AppCompatImageButton( ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply { layoutParams = LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_content_copy_white_24dp)) contentDescription = context.getString(R.string.menu_copy) } init { buildViews() addView(copyButton) addView(showButton) addView(labelView) addView(valueView) } private fun buildViews() { copyButton.apply { id = copyButtonId layoutParams = (layoutParams as LayoutParams?)?.also { it.addRule(ALIGN_PARENT_RIGHT) it.addRule(ALIGN_PARENT_END) } } showButton.apply { id = showButtonId layoutParams = (layoutParams as LayoutParams?)?.also { if (copyButton.isVisible) { it.addRule(LEFT_OF, copyButtonId) it.addRule(START_OF, copyButtonId) } else { it.addRule(ALIGN_PARENT_RIGHT) it.addRule(ALIGN_PARENT_END) } } } labelView.apply { id = labelViewId layoutParams = (layoutParams as LayoutParams?)?.also { it.addRule(LEFT_OF, showButtonId) it.addRule(START_OF, showButtonId) } } valueView.apply { id = valueViewId layoutParams = (layoutParams as LayoutParams?)?.also { it.addRule(LEFT_OF, showButtonId) it.addRule(START_OF, showButtonId) it.addRule(BELOW, labelViewId) } } } override fun applyFontVisibility(fontInVisibility: Boolean) { if (fontInVisibility) valueView.applyFontVisibility() } override var label: String get() { return labelView.text.toString() } set(value) { labelView.text = value } open fun setLabel(@StringRes labelId: Int) { labelView.setText(labelId) } override var value: String get() { return valueView.text.toString() } set(value) { valueView.text = value changeProtectedValueParameters() } open fun setValue(@StringRes valueId: Int) { value = resources.getString(valueId) changeProtectedValueParameters() } override var default: String = "" fun setMaxChars(numberChars: Int) { when { numberChars <= 0 -> { valueView.filters += InputFilter.LengthFilter(MAX_CHARS_LIMIT) } else -> { val chars = if (numberChars > MAX_CHARS_LIMIT) MAX_CHARS_LIMIT else numberChars valueView.filters += InputFilter.LengthFilter(chars) } } } override fun setProtection( protection: Boolean, isCurrentlyProtected: Boolean, onUnprotectClickListener: OnClickListener? ) { super.setProtection(protection, isCurrentlyProtected, onUnprotectClickListener) showButton.isVisible = isProtected if (isProtected) { showButton.setOnClickListener { onUnprotectClickListener?.onClick(this@TextFieldView) } } } override fun changeProtectedValueParameters() { valueView.apply { if (showButton.isVisible) { applyHiddenStyle(isCurrentlyProtected()) } else { linkify() } } invalidate() } private fun linkify() { when { labelView.text.contains(APPLICATION_ID_FIELD_NAME) -> { val packageName = valueView.text.toString() // TODO #996 if (UriUtil.isExternalAppInstalled(context, packageName)) { valueView.customLink { context.openExternalApp(packageName) } //} } else -> { LinkifyCompat.addLinks(valueView, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES) } } } fun getCopyButtonView(): View? { if (copyButton.isVisible) { return copyButton } return null } fun setCopyButtonState(buttonState: ButtonState) { when (buttonState) { ButtonState.ACTIVATE -> { copyButton.apply { visibility = VISIBLE isActivated = false } valueView.apply { isFocusable = true setTextIsSelectable(true) } } ButtonState.DEACTIVATE -> { copyButton.apply { visibility = VISIBLE // Reverse because isActivated show custom color and allow click isActivated = true } valueView.apply { isFocusable = false setTextIsSelectable(false) } } ButtonState.GONE -> { copyButton.apply { visibility = GONE setOnClickListener(null) } valueView.apply { isFocusable = false setTextIsSelectable(false) } } } invalidate() } fun setCopyButtonClickListener(onActionClickListener: ((label: String, value: String) -> Unit)?) { val clickListener = if (onActionClickListener != null) OnClickListener { onActionClickListener.invoke(label, value) } else null setOnActionClickListener(clickListener, null) } override fun setOnActionClickListener( onActionClickListener: OnClickListener?, actionImageId: Int? ) { copyButton.setOnClickListener(onActionClickListener) copyButton.isVisible = onActionClickListener != null invalidate() } override var isFieldVisible: Boolean get() { return isVisible } set(value) { isVisible = value } override fun invalidate() { super.invalidate() buildViews() } enum class ButtonState { ACTIVATE, DEACTIVATE, GONE } companion object { const val MAX_CHARS_LIMIT = Integer.MAX_VALUE } }