mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
feat: Add shield icon as password strength indicator #1355
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
9
app/src/main/res/drawable/ic_shield_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_shield_white_24dp.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user