feat: Add shield icon as password strength indicator #1355

This commit is contained in:
J-Jamet
2024-11-16 12:56:46 +01:00
parent 11199b996c
commit 8133977e09
5 changed files with 173 additions and 34 deletions

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2024 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 package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2024 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.graphics.Color
import android.text.SpannableString
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.style.ImageSpan
import android.util.AttributeSet
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class PasswordTextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: TextFieldView(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
valueView.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
private var indicatorDrawable = ContextCompat.getDrawable(
context,
R.drawable.ic_shield_white_24dp
)?.apply {
val lineHeight = labelView.lineHeight
setBounds(0,0,lineHeight, lineHeight)
DrawableCompat.setTint(this, Color.TRANSPARENT)
}
override var label: String
get() {
return labelView.text.toString().removeSuffix(ICON_STRING_SPACES)
}
set(value) {
indicatorDrawable?.let { drawable ->
val spannableString = SpannableString("$value$ICON_STRING_SPACES")
val startPosition = spannableString.split(ICON_STRING)[0].length
val endPosition = startPosition + ICON_STRING.length
spannableString
.setSpan(
ImageSpan(drawable),
startPosition,
endPosition,
SPAN_EXCLUSIVE_EXCLUSIVE
)
labelView.text = spannableString
} ?: kotlin.run {
labelView.text = value
}
}
override fun setLabel(@StringRes labelId: Int) {
label = resources.getString(labelId)
}
override var value: String
get() {
return valueView.text.toString()
}
set(value) {
val spannableString =
if (PreferencesUtil.colorizePassword(context))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.text = spannableString
changeProtectedValueParameters()
}
override fun setValue(@StringRes valueId: Int) {
value = resources.getString(valueId)
}
private fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
labelView.apply {
post {
val strengthColor = entropyStrength.strength.color
indicatorDrawable?.let { drawable ->
DrawableCompat.setTint(drawable, strengthColor)
}
invalidate()
}
}
}
}
companion object {
private const val ICON_STRING = "[icon]"
private const val ICON_STRING_SPACES = " $ICON_STRING"
}
}

View File

@@ -10,6 +10,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateAttribute import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.OtpModel import com.kunzisoft.keepass.model.OtpModel
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
@@ -48,7 +49,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
field: Field): TextFieldView? { field: Field): TextFieldView? {
// Add an action icon if needed // Add an action icon if needed
return context?.let { return context?.let {
TextFieldView(it).apply { (if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextFieldView(it)
else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility) applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue) setProtection(field.protectedValue.isProtected, mHideProtectedValue)
label = templateAttribute.alias label = templateAttribute.alias

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.text.InputFilter import android.text.InputFilter
import android.text.SpannableString
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
@@ -38,15 +37,11 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
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.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
class TextFieldView @JvmOverloads constructor(context: Context, open class TextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView { : RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
@@ -56,7 +51,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
private var showButtonId = ViewCompat.generateViewId() private var showButtonId = ViewCompat.generateViewId()
private var copyButtonId = ViewCompat.generateViewId() private var copyButtonId = ViewCompat.generateViewId()
private val labelView = AppCompatTextView(context).apply { protected val labelView = AppCompatTextView(context).apply {
setTextAppearance(context, setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_LabelTextStyle) R.style.KeepassDXStyle_TextAppearance_LabelTextStyle)
layoutParams = LayoutParams( layoutParams = LayoutParams(
@@ -77,7 +72,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
} }
} }
} }
private val valueView = AppCompatTextView(context).apply { protected val valueView = AppCompatTextView(context).apply {
setTextAppearance(context, setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_TextNode) R.style.KeepassDXStyle_TextAppearance_TextNode)
layoutParams = LayoutParams( layoutParams = LayoutParams(
@@ -131,46 +126,46 @@ class TextFieldView @JvmOverloads constructor(context: Context,
private fun buildViews() { private fun buildViews() {
copyButton.apply { copyButton.apply {
id = copyButtonId id = copyButtonId
layoutParams = (layoutParams as LayoutParams?).also { layoutParams = (layoutParams as LayoutParams?)?.also {
it?.addRule(ALIGN_PARENT_RIGHT) it.addRule(ALIGN_PARENT_RIGHT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(ALIGN_PARENT_END) it.addRule(ALIGN_PARENT_END)
} }
} }
} }
showButton.apply { showButton.apply {
id = showButtonId id = showButtonId
layoutParams = (layoutParams as LayoutParams?).also { layoutParams = (layoutParams as LayoutParams?)?.also {
if (copyButton.isVisible) { if (copyButton.isVisible) {
it?.addRule(LEFT_OF, copyButtonId) it.addRule(LEFT_OF, copyButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, copyButtonId) it.addRule(START_OF, copyButtonId)
} }
} else { } else {
it?.addRule(ALIGN_PARENT_RIGHT) it.addRule(ALIGN_PARENT_RIGHT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(ALIGN_PARENT_END) it.addRule(ALIGN_PARENT_END)
} }
} }
} }
} }
labelView.apply { labelView.apply {
id = labelViewId id = labelViewId
layoutParams = (layoutParams as LayoutParams?).also { layoutParams = (layoutParams as LayoutParams?)?.also {
it?.addRule(LEFT_OF, showButtonId) it.addRule(LEFT_OF, showButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, showButtonId) it.addRule(START_OF, showButtonId)
} }
} }
} }
valueView.apply { valueView.apply {
id = valueViewId id = valueViewId
layoutParams = (layoutParams as LayoutParams?).also { layoutParams = (layoutParams as LayoutParams?)?.also {
it?.addRule(LEFT_OF, showButtonId) it.addRule(LEFT_OF, showButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, showButtonId) it.addRule(START_OF, showButtonId)
} }
it?.addRule(BELOW, labelViewId) it.addRule(BELOW, labelViewId)
} }
} }
} }
@@ -188,7 +183,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
labelView.text = value labelView.text = value
} }
fun setLabel(@StringRes labelId: Int) { open fun setLabel(@StringRes labelId: Int) {
labelView.setText(labelId) labelView.setText(labelId)
} }
@@ -197,17 +192,11 @@ class TextFieldView @JvmOverloads constructor(context: Context,
return valueView.text.toString() return valueView.text.toString()
} }
set(value) { set(value) {
val spannableString = valueView.text = value
if (PreferencesUtil.colorizePassword(context)
&& TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.text = spannableString
changeProtectedValueParameters() changeProtectedValueParameters()
} }
fun setValue(@StringRes valueId: Int) { open fun setValue(@StringRes valueId: Int) {
value = resources.getString(valueId) value = resources.getString(valueId)
changeProtectedValueParameters() changeProtectedValueParameters()
} }
@@ -237,7 +226,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
invalidate() invalidate()
} }
private fun changeProtectedValueParameters() { protected fun changeProtectedValueParameters() {
valueView.apply { valueView.apply {
if (showButton.isVisible) { if (showButton.isVisible) {
applyHiddenStyle(showButton.isSelected) applyHiddenStyle(showButton.isSelected)

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
</vector>