Manage and autofill credit card details

This commit is contained in:
Ulrich Dürholz
2021-03-31 22:11:05 +02:00
parent c9c739fd52
commit 2ba8702787
19 changed files with 988 additions and 61 deletions

View File

@@ -48,6 +48,7 @@ import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.CreditCardCustomFields
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -316,6 +317,7 @@ class EntryActivity : LockingActivity() {
// Assign custom fields
if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields()
entryContentsView?.clearCreditCardFields()
entryInfo.customFields.forEach { field ->
val label = field.name
// OTP field is already managed in dedicated view
@@ -326,7 +328,7 @@ class EntryActivity : LockingActivity() {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
getString(R.string.copy_field, CreditCardCustomFields.getLocalizedName(applicationContext, field.name))
)
}
} else {
@@ -340,6 +342,7 @@ class EntryActivity : LockingActivity() {
}
}
}
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
@@ -433,15 +436,15 @@ class EntryActivity : LockingActivity() {
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
entryFieldCopyView,
{
val appNameString = getString(R.string.app_name)
clipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
},
{
performedNextEducation(entryActivityEducation, menu)
})
entryFieldCopyView,
{
val appNameString = getString(R.string.app_name)
clipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
},
{
performedNextEducation(entryActivityEducation, menu)
})
if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)

View File

@@ -57,6 +57,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
@@ -81,6 +82,7 @@ import kotlin.collections.ArrayList
class EntryEditActivity : LockingActivity(),
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
CreditCardDetailsDialogFragment.EntryCCFieldListener,
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
@@ -405,6 +407,11 @@ class EntryEditActivity : LockingActivity(),
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
}
private fun addNewCreditCard() {
val cc = CreditCard(entryEditFragment?.getExtraFields())
CreditCardDetailsDialogFragment.build(cc).show(supportFragmentManager, "CreditCardDialog")
}
/**
* Add a new customized field
*/
@@ -455,6 +462,12 @@ class EntryEditActivity : LockingActivity(),
entryEditFragment?.removeExtraField(oldField)
}
override fun onNewCCFieldsApproved(ccFields: ArrayList<Field>) {
for (field in ccFields) {
entryEditFragment?.putExtraField(field)
}
}
/**
* Add a new attachment
*/
@@ -609,8 +622,14 @@ class EntryEditActivity : LockingActivity(),
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
menu?.findItem(R.id.menu_add_field)?.apply {
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
menu?.findItem(R.id.menu_add_cc)?.apply {
isEnabled = allowCustomField
isVisible = allowCustomField
}
@@ -682,6 +701,10 @@ class EntryEditActivity : LockingActivity(),
addNewCustomField()
return true
}
R.id.menu_add_cc -> {
addNewCreditCard()
return true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
return true

View File

@@ -0,0 +1,209 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.CreditCardCustomFields.buildAllFields
import com.kunzisoft.keepass.model.CreditCard
class CreditCardDetailsDialogFragment : DialogFragment() {
private var mCreditCard: CreditCard? = null
private var entryCCFieldListener: EntryCCFieldListener? = null
private var mCcCardholderName: EditText? = null
private var mCcCardNumber: EditText? = null
private var mCcSecurityCode: EditText? = null
private var mCcExpirationMonthSpinner: Spinner? = null
private var mCcExpirationYearSpinner: Spinner? = null
private var mCcCardNumberWellFormed: Boolean = false
private var mCcSecurityCodeWellFormed: Boolean = false
private var mPositiveButton: Button? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
entryCCFieldListener = context as EntryCCFieldListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + EntryCCFieldListener::class.java.name)
}
}
override fun onDetach() {
entryCCFieldListener = null
super.onDetach()
}
override fun onResume() {
super.onResume()
// To prevent auto dismiss
val d = dialog as AlertDialog?
if (d != null) {
mPositiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
mPositiveButton?.run {
isEnabled = mCcSecurityCodeWellFormed && mCcCardNumberWellFormed
attachListeners()
setOnClickListener {
submitDialog()
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_CREDIT_CARD, mCreditCard)
}
private fun submitDialog() {
val ccNumber = mCcCardNumber?.text?.toString() ?: ""
val month = mCcExpirationMonthSpinner?.selectedItem?.toString() ?: ""
val year = mCcExpirationYearSpinner?.selectedItem?.toString() ?: ""
val cvv = mCcSecurityCode?.text?.toString() ?: ""
val ccName = mCcCardholderName?.text?.toString() ?: ""
entryCCFieldListener?.onNewCCFieldsApproved(buildAllFields(ccName, ccNumber, month + year, cvv))
(dialog as AlertDialog?)?.dismiss()
}
interface EntryCCFieldListener {
fun onNewCCFieldsApproved(ccFields: ArrayList<Field>)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Retrieve credit card details if available
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(KEY_CREDIT_CARD)) {
mCreditCard = savedInstanceState.getParcelable(KEY_CREDIT_CARD)
}
} else {
arguments?.apply {
if (containsKey(KEY_CREDIT_CARD)) {
mCreditCard = getParcelable(KEY_CREDIT_CARD)
}
}
}
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.entry_cc_details_dialog, null)
mCcCardholderName = root?.findViewById(R.id.creditCardholderNameField)
mCcExpirationMonthSpinner = root?.findViewById(R.id.expirationMonth)
mCcExpirationYearSpinner = root?.findViewById(R.id.expirationYear)
mCcCardNumber = root?.findViewById(R.id.creditCardNumberField)
mCcSecurityCode = root?.findViewById(R.id.creditCardSecurityCode)
mCreditCard?.let {
mCcCardholderName!!.setText(it.cardholder)
mCcCardNumberWellFormed = true
mCcCardNumber!!.setText(it.number)
mCcSecurityCodeWellFormed = true
mCcSecurityCode!!.setText(it.cvv)
}
val monthAdapter = ArrayAdapter.createFromResource(requireContext(),
R.array.month_array, android.R.layout.simple_spinner_item)
monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
mCcExpirationMonthSpinner!!.adapter = monthAdapter
mCreditCard?.let { mCcExpirationMonthSpinner!!.setSelection(
getIndex(mCcExpirationMonthSpinner!!, it.getExpirationMonth()) ) }
val yearAdapter = ArrayAdapter.createFromResource(requireContext(),
R.array.year_array, android.R.layout.simple_spinner_item)
yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
mCcExpirationYearSpinner!!.adapter = yearAdapter
mCreditCard?.let { mCcExpirationYearSpinner!!.setSelection(
getIndex(mCcExpirationYearSpinner!!, it.getExpirationYear()) ) }
val builder = AlertDialog.Builder(activity)
builder.setView(root).setTitle(R.string.entry_setup_cc)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val dialogCreated = builder.create()
dialogCreated.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
return dialogCreated
}
return super.onCreateDialog(savedInstanceState)
}
private fun getIndex(spinner: Spinner, value: String?): Int {
for (i in 0 until spinner.count) {
if (spinner.getItemAtPosition(i).toString().equals(value, ignoreCase = true)) {
return i
}
}
return 0
}
private fun attachListeners() {
mCcCardNumber?.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val userString = s?.toString()
mCcCardNumberWellFormed = userString?.length == 16
mPositiveButton?.run {
isEnabled = mCcSecurityCodeWellFormed && mCcCardNumberWellFormed
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
mCcSecurityCode?.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val userString = s?.toString()
mCcSecurityCodeWellFormed = (userString?.length == 3 || userString?.length == 4)
mPositiveButton?.run {
isEnabled = mCcSecurityCodeWellFormed && mCcCardNumberWellFormed
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
companion object {
private const val KEY_CREDIT_CARD = "KEY_CREDIT_CARD"
fun build(creditCard: CreditCard? = null): CreditCardDetailsDialogFragment {
return CreditCardDetailsDialogFragment().apply {
if (creditCard != null) {
arguments = Bundle().apply {
putParcelable(KEY_CREDIT_CARD, creditCard)
}
}
}
}
}
}

View File

@@ -46,6 +46,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.CreditCardCustomFields
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ExpirationView
@@ -53,7 +54,7 @@ import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
class EntryEditFragment: StylishFragment() {
class EntryEditFragment : StylishFragment() {
private lateinit var entryTitleLayoutView: TextInputLayout
private lateinit var entryTitleView: EditText
@@ -91,7 +92,7 @@ class EntryEditFragment: StylishFragment() {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit_contents, container, false)
.inflate(R.layout.fragment_entry_edit_contents, container, false)
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
@@ -152,7 +153,8 @@ class EntryEditFragment: StylishFragment() {
}
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD)
?: mLastFocusedEditField
}
populateViewsWithEntry()
@@ -185,7 +187,8 @@ class EntryEditFragment: StylishFragment() {
{
try {
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
} catch (ignore: Exception) {}
} catch (ignore: Exception) {
}
}
)
}
@@ -306,7 +309,7 @@ class EntryEditFragment: StylishFragment() {
*/
private var mExtraFieldsList: MutableList<Field> = ArrayList()
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
private var mOnEditButtonClickListener: ((item: Field) -> Unit)? = null
private fun buildViewFromField(extraField: Field): View? {
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
@@ -316,7 +319,15 @@ class EntryEditFragment: StylishFragment() {
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
extraFieldValueContainer?.hint = extraField.name
when (extraField.name) {
CreditCardCustomFields.CC_CARDHOLDER_FIELD_NAME -> extraFieldValueContainer?.hint = context?.getString(R.string.cc_cardholder)
CreditCardCustomFields.CC_EXP_FIELD_NAME -> extraFieldValueContainer?.hint = context?.getString(R.string.cc_expiration)
CreditCardCustomFields.CC_NUMBER_FIELD_NAME -> extraFieldValueContainer?.hint = context?.getString(R.string.cc_number)
CreditCardCustomFields.CC_CVV_FIELD_NAME -> extraFieldValueContainer?.hint = context?.getString(R.string.cc_security_code)
else -> extraFieldValueContainer?.hint = extraField.name
}
extraFieldValueContainer?.id = View.NO_ID
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
@@ -365,21 +376,24 @@ class EntryEditFragment: StylishFragment() {
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>,
onEditButtonClickListener: ((item: Field)->Unit)?) {
onEditButtonClickListener: ((item: Field) -> Unit)?) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
mExtraFieldsList.clear()
mExtraFieldsList.addAll(fields)
extraFieldsListView.removeAllViews()
fields.forEach {
extraFieldsListView.addView(buildViewFromField(it))
}
// Request last focus
mLastFocusedEditField?.let { focusField ->
mExtraViewToRequestFocus?.apply {
requestFocus()
setSelection(focusField.cursorSelectionStart,
focusField.cursorSelectionEnd)
focusField.cursorSelectionEnd)
}
}
mLastFocusedEditField = null
@@ -457,7 +471,7 @@ class EntryEditFragment: StylishFragment() {
fun assignAttachments(attachments: List<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
onDeleteItem: (attachment: Attachment) -> Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onDeleteButtonClickListener = { item ->
@@ -474,7 +488,7 @@ class EntryEditFragment: StylishFragment() {
}
fun putAttachment(attachment: EntryAttachmentState,
onPreviewLoaded: (()-> Unit)? = null) {
onPreviewLoaded: (() -> Unit)? = null) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment)
attachmentsAdapter.onBinaryPreviewLoaded = {

View File

@@ -48,8 +48,11 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.CreditCardCustomFields
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
import kotlin.collections.ArrayList
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -103,9 +106,9 @@ object AutofillHelper {
}
private fun buildDataset(context: Context,
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views)
@@ -114,8 +117,89 @@ object AutofillHelper {
struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
}
struct.passwordId?.let { password ->
builder.setValue(password, AutofillValue.forText(entryInfo.password))
struct.passwordId?.let { passwordId ->
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
}
for (field in entryInfo.customFields) {
if (field.name == CreditCardCustomFields.CC_CARDHOLDER_FIELD_NAME) {
struct.ccNameId?.let { ccNameId ->
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
if (field.name == CreditCardCustomFields.CC_NUMBER_FIELD_NAME) {
struct.ccnId?.let { ccnId ->
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
if (field.name == CreditCardCustomFields.CC_EXP_FIELD_NAME) {
// the database stores the expiration month and year as a String
// of length four in the format MMYY
if (field.protectedValue.stringValue.length != 4) continue
// get month (month in database entry is stored as String in the range 01..12)
val monthString = field.protectedValue.stringValue.substring(0, 2)
if (monthString !in context.resources.getStringArray(R.array.month_array)) continue
val month = monthString.toInt()
// get year (year in database entry is stored as String in the range 20..29)
val yearString = field.protectedValue.stringValue.substring(2, 4)
if (yearString !in context.resources.getStringArray(R.array.year_array)) continue
struct.ccExpDateId?.let {
if (struct.isWebView) {
// set date string as defined in https://html.spec.whatwg.org
val dateString = "20$yearString\u002D$monthString"
builder.setValue(it, AutofillValue.forText(dateString))
} else {
val calendar = Calendar.getInstance()
calendar.clear()
val year = "20$yearString".toInt()
calendar[Calendar.YEAR] = year
// Month value is 0-based. e.g., 0 for January
calendar[Calendar.MONTH] = month - 1
val date = calendar.timeInMillis
builder.setValue(it, AutofillValue.forDate(date))
}
}
struct.ccExpDateMonthId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(month.toString()))
} else {
if (struct.ccExpMonthOptions != null) {
// index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1))
} else {
builder.setValue(it, AutofillValue.forText(month.toString()))
}
}
}
struct.ccExpDateYearId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(yearString))
} else {
if (struct.ccExpYearOptions != null) {
var yearIndex = struct.ccExpYearOptions!!.indexOf(yearString)
if (yearIndex == -1) {
yearIndex = struct.ccExpYearOptions!!.indexOf("20$yearString")
}
if (yearIndex != -1) {
builder.setValue(it, AutofillValue.forList(yearIndex))
} else {
builder.setValue(it, AutofillValue.forText(yearString))
}
} else {
builder.setValue(it, AutofillValue.forText(yearString))
}
}
}
}
if (field.name == CreditCardCustomFields.CC_CVV_FIELD_NAME) {
struct.cvvId?.let { cvvId ->
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -126,8 +210,8 @@ object AutofillHelper {
return try {
builder.build()
} catch (e: IllegalArgumentException) {
// if not value be set
} catch (e: Exception) {
// at least one value must be set
null
}
}
@@ -156,7 +240,7 @@ object AutofillHelper {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion-1
if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
@@ -191,7 +275,7 @@ object AutofillHelper {
fun buildResponse(context: Context,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -208,6 +292,7 @@ object AutofillHelper {
}
}
}
// Add inline suggestion for new IME and dataset
entriesInfo.forEachIndexed { index, entryInfo ->
val inlinePresentation = inlineSuggestionsRequest?.let {
@@ -217,9 +302,17 @@ object AutofillHelper {
null
}
}
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
val dataSet = buildDataset(context, entryInfo, parseResult, inlinePresentation)
dataSet?.let {
responseBuilder.addDataset(it)
}
}
return try {
responseBuilder.build()
} catch (e: java.lang.Exception) {
null
}
return responseBuilder.build()
}
/**

View File

@@ -101,6 +101,10 @@ class KeeAutofillService : AutofillService() {
callback)
}
}
// else {
// TODO: Disable autofill for the app for API level >= 28
// public FillResponse.Builder disableAutofill (long duration)
// }
}
}
}

View File

@@ -21,12 +21,13 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.os.Build
import android.text.InputType
import androidx.annotation.RequiresApi
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
import java.util.*
import kotlin.collections.ArrayList
/**
@@ -36,9 +37,7 @@ import java.util.*
class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameNeeded = true
private var usernameCandidate: AutofillId? = null
private var usernameIdCandidate: AutofillId? = null
private var usernameValueCandidate: AutofillValue? = null
fun parse(saveValue: Boolean = false): Result? {
@@ -46,7 +45,7 @@ class StructureParser(private val structure: AssistStructure) {
result = Result()
result?.apply {
allowSaveValues = saveValue
usernameCandidate = null
usernameIdCandidate = null
usernameValueCandidate = null
mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
@@ -57,26 +56,26 @@ class StructureParser(private val structure: AssistStructure) {
break@mainLoop
}
// If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameCandidate != null) {
usernameId = usernameCandidate
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
usernameId = usernameIdCandidate
if (allowSaveValues) {
usernameValue = usernameValueCandidate
}
}
}
// Return the result only if password field is retrieved
return if ((!usernameNeeded || result?.usernameId != null)
&& result?.passwordId != null)
result
else
null
return result
} catch (e: Exception) {
return null
}
}
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
// remember this
if (node.className == "android.webkit.WebView") {
result?.isWebView = true
}
// Get the domain of a web app
node.webDomain?.let { webDomain ->
if (webDomain.isNotEmpty()) {
@@ -97,8 +96,7 @@ class StructureParser(private val structure: AssistStructure) {
var returnValue = false
// Only parse visible nodes
if (node.visibility == View.VISIBLE) {
if (node.autofillId != null
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
if (node.autofillId != null) {
// Parse methods
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
@@ -130,7 +128,7 @@ class StructureParser(private val structure: AssistStructure) {
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|| it.contains("email", true)
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username hint")
@@ -139,14 +137,70 @@ class StructureParser(private val structure: AssistStructure) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password hint")
// Username not needed in this case
usernameNeeded = false
return true
}
it == "cc-name" -> {
result?.ccNameId = autofillId
result?.ccNameValue = node.autofillValue
return true
}
it == View.AUTOFILL_HINT_CREDIT_CARD_NUMBER || it == "cc-number" -> {
result?.ccnId = autofillId
result?.ccnValue = node.autofillValue
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_NUMBER hint")
return true
}
it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE || it == "cc-exp" -> {
result?.ccExpDateId = autofillId
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE hint")
return true
}
it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR || it == "cc-exp-year" -> {
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR hint")
result?.ccExpDateYearId = autofillId
if (node.autofillValue != null) {
if (node.autofillValue?.isText == true) {
try {
result?.ccExpDateYearValue =
node.autofillValue?.textValue.toString().toInt()
} catch (e: Exception) {
result?.ccExpDateYearValue = 0
}
}
}
if (node.autofillOptions != null) {
result?.ccExpYearOptions = node.autofillOptions
}
return true
}
it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH || it == "cc-exp-month" -> {
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH hint")
result?.ccExpDateMonthId = autofillId
if (node.autofillValue != null) {
if (node.autofillValue?.isText == true) {
try {
result?.ccExpDateMonthValue =
node.autofillValue?.textValue.toString().toInt()
} catch (e: Exception) {
result?.ccExpDateMonthValue = 0
}
}
}
if (node.autofillOptions != null) {
result?.ccExpMonthOptions = node.autofillOptions
}
return true
}
it == View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE || it == "cc-csc" -> {
result?.cvvId = autofillId
result?.cvvValue = node.autofillValue
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE hint")
return true
}
// Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
it.equals("off", true) ||
it.equals("on", true) -> {
it.equals("on", true) -> {
Log.d(TAG, "Autofill web hint")
return parseNodeByHtmlAttributes(node)
}
@@ -171,7 +225,7 @@ class StructureParser(private val structure: AssistStructure) {
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameCandidate = autofillId
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
@@ -219,7 +273,7 @@ class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_NORMAL,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
usernameCandidate = autofillId
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
@@ -230,7 +284,6 @@ class StructureParser(private val structure: AssistStructure) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true
}
inputIsVariationType(inputType,
@@ -252,16 +305,15 @@ class StructureParser(private val structure: AssistStructure) {
when {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameCandidate = autofillId
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true
}
else -> {
@@ -275,6 +327,7 @@ class StructureParser(private val structure: AssistStructure) {
@RequiresApi(api = Build.VERSION_CODES.O)
class Result {
var isWebView: Boolean = false
var applicationId: String? = null
var webDomain: String? = null
@@ -289,6 +342,11 @@ class StructureParser(private val structure: AssistStructure) {
field = value
}
// if the user selects the credit card expiration date from a list of options
// all options are stored here
var ccExpMonthOptions: Array<CharSequence>? = null
var ccExpYearOptions: Array<CharSequence>? = null
var usernameId: AutofillId? = null
set(value) {
if (field == null)
@@ -301,6 +359,42 @@ class StructureParser(private val structure: AssistStructure) {
field = value
}
var ccNameId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var ccnId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var ccExpDateId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var ccExpDateYearId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var ccExpDateMonthId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var cvvId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
usernameId?.let {
@@ -309,6 +403,24 @@ class StructureParser(private val structure: AssistStructure) {
passwordId?.let {
all.add(it)
}
ccNameId?.let {
all.add(it)
}
ccnId?.let {
all.add(it)
}
ccExpDateId?.let {
all.add(it)
}
ccExpDateYearId?.let {
all.add(it)
}
ccExpDateMonthId?.let {
all.add(it)
}
cvvId?.let {
all.add(it)
}
return all.toTypedArray()
}
@@ -326,6 +438,41 @@ class StructureParser(private val structure: AssistStructure) {
if (allowSaveValues && field == null)
field = value
}
// stores name of cardholder
var ccNameValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
field = value
}
// stores credit card number
var ccnValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
field = value
}
// for year of CC expiration date
var ccExpDateYearValue = 0
set(value) {
if (allowSaveValues)
field = value
}
// for month of CC expiration date
var ccExpDateMonthValue = 0
set(value) {
if (allowSaveValues)
field = value
}
// the security code for the credit card (also called CVV)
var cvvValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
field = value
}
}
companion object {

View File

@@ -0,0 +1,94 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
class CreditCard() : Parcelable {
var cardholder: String = "";
var number: String = "";
var expiration: String = "";
var cvv: String = "";
constructor(ccFields: List<Field>?) : this() {
ccFields?.let {
for (field in it) {
when (field.name) {
CreditCardCustomFields.CC_CARDHOLDER_FIELD_NAME ->
this.cardholder = field.protectedValue.stringValue
CreditCardCustomFields.CC_NUMBER_FIELD_NAME ->
this.number = field.protectedValue.stringValue
CreditCardCustomFields.CC_EXP_FIELD_NAME ->
this.expiration = field.protectedValue.stringValue
CreditCardCustomFields.CC_CVV_FIELD_NAME ->
this.cvv = field.protectedValue.stringValue
}
}
}
}
constructor(parcel: Parcel) : this() {
cardholder = parcel.readString() ?: cardholder
number = parcel.readString() ?: number
expiration = parcel.readString() ?: expiration
cvv = parcel.readString() ?: cvv
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(cardholder)
parcel.writeString(number)
parcel.writeString(expiration)
parcel.writeString(cvv)
}
fun getExpirationMonth(): String {
return if (expiration.length == 4) {
expiration.substring(0, 2)
} else {
""
}
}
fun getExpirationYear(): String {
return if (expiration.length == 4) {
expiration.substring(2, 4)
} else {
""
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CreditCard
if (cardholder != other.cardholder) return false
if (number != other.number) return false
if (expiration != other.expiration) return false
if (cvv != other.cvv) return false
return true
}
override fun describeContents(): Int {
return 0
}
override fun hashCode(): Int {
var result = cardholder.hashCode()
result = 31 * result + number.hashCode()
result = 31 * result + expiration.hashCode()
result = 31 * result + cvv.hashCode()
return result
}
companion object CREATOR : Parcelable.Creator<CreditCard> {
override fun createFromParcel(parcel: Parcel): CreditCard {
return CreditCard(parcel)
}
override fun newArray(size: Int): Array<CreditCard?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,35 @@
package com.kunzisoft.keepass.model
import android.content.Context
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.security.ProtectedString
object CreditCardCustomFields {
const val CC_CARDHOLDER_FIELD_NAME = "CREDIT_CARD_CARDHOLDER"
const val CC_NUMBER_FIELD_NAME = "CREDIT_CARD_NUMBER"
const val CC_EXP_FIELD_NAME = "CREDIT_CARD_EXPIRATION"
const val CC_CVV_FIELD_NAME = "CREDIT_CARD_CVV"
val CC_CUSTOM_FIELDS = arrayOf(CC_CARDHOLDER_FIELD_NAME, CC_NUMBER_FIELD_NAME,
CC_EXP_FIELD_NAME, CC_CVV_FIELD_NAME)
fun getLocalizedName(context: Context, fieldName: String): String {
return when (fieldName) {
CC_CARDHOLDER_FIELD_NAME -> context.getString(R.string.cc_cardholder)
CC_NUMBER_FIELD_NAME -> context.getString(R.string.cc_number)
CC_EXP_FIELD_NAME -> context.getString(R.string.cc_expiration)
CC_CVV_FIELD_NAME -> context.getString(R.string.cc_security_code)
else -> fieldName
}
}
fun buildAllFields(cardholder: String, number: String, expiration: String, cvv: String): ArrayList<Field> {
val ccnField = Field(CC_NUMBER_FIELD_NAME, ProtectedString(false, number))
val expirationField = Field(CC_EXP_FIELD_NAME, ProtectedString(false, expiration))
val cvvField = Field(CC_CVV_FIELD_NAME, ProtectedString(true, cvv))
val ccNameField = Field(CC_CARDHOLDER_FIELD_NAME, ProtectedString(false, cardholder))
return arrayListOf(ccNameField, ccnField, expirationField, cvvField)
}
}

View File

@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.search.UuidUtil
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.model.CreditCardCustomFields
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -55,6 +56,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private var fontInVisibility: Boolean = false
private val entryFieldsContainerView: View
private val userNameFieldView: EntryField
private val passwordFieldView: EntryField
private val otpFieldView: EntryField
@@ -66,6 +69,9 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val extraFieldsContainerView: View
private val extraFieldsListView: ViewGroup
private val creditCardContainerView: View
private val creditCardFieldsListView: ViewGroup
private val creationDateView: TextView
private val modificationDateView: TextView
private val expiresImageView: ImageView
@@ -87,6 +93,9 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_contents, this)
entryFieldsContainerView = findViewById(R.id.entry_fields_container)
entryFieldsContainerView.visibility = View.GONE
userNameFieldView = findViewById(R.id.entry_user_name_field)
userNameFieldView.setLabel(R.string.entry_user_name)
@@ -107,6 +116,9 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
extraFieldsListView = findViewById(R.id.extra_fields_list)
creditCardContainerView = findViewById(R.id.credit_card_container)
creditCardFieldsListView = findViewById(R.id.credit_card_fields_list)
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list)
attachmentsListView?.apply {
@@ -157,6 +169,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
visibility = View.VISIBLE
setValue(userName)
applyFontVisibility(fontInVisibility)
showOrHideEntryFieldsContainer(false)
} else {
visibility = View.GONE
}
@@ -173,7 +186,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
setValue(password, true)
applyFontVisibility(fontInVisibility)
activateCopyButton(allowCopyPassword)
}else {
showOrHideEntryFieldsContainer(false)
} else {
visibility = View.GONE
}
assignCopyButtonClickListener(onClickListener)
@@ -220,6 +234,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
}
}
showOrHideEntryFieldsContainer(false)
} else {
otpFieldView.visibility = View.GONE
otpProgressView?.visibility = View.GONE
@@ -231,6 +246,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
if (url != null && url.isNotEmpty()) {
visibility = View.VISIBLE
setValue(url)
showOrHideEntryFieldsContainer(false)
} else {
visibility = View.GONE
}
@@ -243,6 +259,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
visibility = View.VISIBLE
setValue(notes)
applyFontVisibility(fontInVisibility)
showOrHideEntryFieldsContainer(false)
} else {
visibility = View.GONE
}
@@ -283,6 +300,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
}
private fun showOrHideEntryFieldsContainer(hide: Boolean) {
entryFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
}
/* -------------
* Extra Fields
* -------------
@@ -292,11 +313,19 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
extraFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
}
private fun showOrHideCreditCardContainer(hide: Boolean) {
creditCardContainerView.visibility = if (hide) View.GONE else View.VISIBLE
}
fun addExtraField(title: String,
value: ProtectedString,
allowCopy: Boolean,
onCopyButtonClickListener: OnClickListener?) {
if (title in CreditCardCustomFields.CC_CUSTOM_FIELDS) {
return addExtraCCField(title, value, allowCopy, onCopyButtonClickListener)
}
val entryCustomField: EntryField? = EntryField(context)
entryCustomField?.apply {
setLabel(title)
@@ -306,17 +335,48 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
assignCopyButtonClickListener(onCopyButtonClickListener)
applyFontVisibility(fontInVisibility)
}
entryCustomField?.let {
extraFieldsListView.addView(it)
}
showOrHideExtraFieldsContainer(false)
}
private fun addExtraCCField(fieldName: String,
value: ProtectedString,
allowCopy: Boolean,
onCopyButtonClickListener: OnClickListener?) {
val label = CreditCardCustomFields.getLocalizedName(context, fieldName)
val entryCustomField: EntryField? = EntryField(context)
entryCustomField?.apply {
setLabel(label)
setValue(value.toString(), value.isProtected)
activateCopyButton(allowCopy)
assignCopyButtonClickListener(onCopyButtonClickListener)
applyFontVisibility(fontInVisibility)
checkCreditCardDetails(fieldName)
}
entryCustomField?.let {
creditCardFieldsListView.addView(it)
}
showOrHideCreditCardContainer(false)
}
fun clearExtraFields() {
extraFieldsListView.removeAllViews()
showOrHideExtraFieldsContainer(true)
}
fun clearCreditCardFields() {
creditCardFieldsListView.removeAllViews()
showOrHideCreditCardContainer(true)
}
/* -------------
* Attachments
* -------------
@@ -332,7 +392,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
fun assignAttachments(attachments: Set<Attachment>,
streamDirection: StreamDirection,
onAttachmentClicked: (attachment: Attachment)->Unit) {
onAttachmentClicked: (attachment: Attachment) -> Unit) {
showAttachments(attachments.isNotEmpty())
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onItemClickListener = { item ->
@@ -349,7 +409,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
* -------------
*/
fun assignHistory(history: ArrayList<Entry>, action: (historyItem: Entry, position: Int)->Unit) {
fun assignHistory(history: ArrayList<Entry>, action: (historyItem: Entry, position: Int) -> Unit) {
historyAdapter.clear()
historyAdapter.entryHistoryList.addAll(history)
historyAdapter.onItemClickListener = { item, position ->

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
@@ -31,6 +32,7 @@ import androidx.annotation.StringRes
import androidx.core.text.util.LinkifyCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.model.CreditCardCustomFields
import com.kunzisoft.keepass.utils.UriUtil
class EntryField @JvmOverloads constructor(context: Context,
@@ -106,6 +108,23 @@ class EntryField @JvmOverloads constructor(context: Context,
}
}
private fun setValueTextColor(color: Int) {
valueView.setTextColor(color)
}
fun checkCreditCardDetails(fieldName: String) {
val value = valueView.text
when (fieldName) {
CreditCardCustomFields.CC_CVV_FIELD_NAME ->
if (value.length < 3 || value.length > 4) setValueTextColor(Color.RED)
CreditCardCustomFields.CC_EXP_FIELD_NAME ->
if (value.length != 4) setValueTextColor(Color.RED)
CreditCardCustomFields.CC_NUMBER_FIELD_NAME ->
if (value.length != 16) setValueTextColor(Color.RED)
}
}
fun setAutoLink() {
if (!isProtected) linkify()
changeProtectedValueParameters()

View File

@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
/**
* Replace font by monospace, must be called after seText()
* Replace font by monospace, must be called after setText()
*/
fun TextView.applyFontVisibility() {
val typeFace = Typeface.createFromAsset(context.assets, "fonts/FiraMono-Regular.ttf")

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,4L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,6c0,-1.11 -0.89,-2 -2,-2zM20,18L4,18v-6h16v6zM20,8L4,8L4,6h16v2z"/>
</vector>

View File

@@ -83,6 +83,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/validate"
android:layout_marginBottom="?attr/actionBarSize"
android:src="@drawable/ic_check_white_24dp"
app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2019 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/>.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
android:orientation="vertical"
android:padding="@dimen/default_margin"
tools:targetApi="o">
<androidx.cardview.widget.CardView
android:id="@+id/card_view_cc_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="4dp"
app:cardCornerRadius="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="UnusedAttribute">
<TextView
android:id="@+id/creditCardholderNameLabel"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:labelFor="@+id/creditCardNumberField"
android:text="@string/cc_cardholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/creditCardholderNameField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:focusedByDefault="true"
android:hint="@string/cc_cardholder"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="textPersonName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardholderNameLabel" />
<TextView
android:id="@+id/creditCardNumberLabel"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:labelFor="@+id/creditCardNumberField"
android:text="@string/cc_number"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardholderNameField" />
<EditText
android:id="@+id/creditCardNumberField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:focusedByDefault="true"
android:hint="@string/cc_number"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="number"
android:maxLength="16"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardNumberLabel" />
<TextView
android:id="@+id/creditCardExpirationLabel"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:text="@string/cc_expiration"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardNumberField" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/expirationMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
app:layout_constraintEnd_toStartOf="@+id/expirationYear"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardExpirationLabel" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/expirationYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:minHeight="48dp"
app:layout_constraintStart_toEndOf="@+id/expirationMonth"
app:layout_constraintTop_toBottomOf="@+id/creditCardExpirationLabel" />
<TextView
android:id="@+id/creditCardSecurityCodeLabel"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:labelFor="@+id/creditCardSecurityCode"
android:text="@string/cc_security_code"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/expirationYear" />
<!-- American Express has four digits? -->
<EditText
android:id="@+id/creditCardSecurityCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/creditCardSecurityCodeLabel"
android:ems="6"
android:hint="@string/cc_security_code"
android:inputType="number"
android:maxLength="4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardSecurityCodeLabel" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -25,6 +25,7 @@
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/entry_fields_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_view_margin"
@@ -96,6 +97,25 @@
android:orientation="vertical" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/credit_card_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin"
android:layout_marginLeft="@dimen/card_view_margin"
android:layout_marginEnd="@dimen/card_view_margin"
android:layout_marginRight="@dimen/card_view_margin"
android:layout_marginBottom="@dimen/card_view_margin_bottom"
android:visibility="gone">
<LinearLayout
android:id="@+id/credit_card_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
android:id="@+id/entry_attachments_container"
android:layout_width="match_parent"

View File

@@ -39,4 +39,9 @@
android:orderInCategory="94"
app:iconTint="?attr/colorControlNormal"
app:showAsAction="always" />
<item android:id="@+id/menu_add_cc"
android:icon="@drawable/ic_baseline_credit_card_24"
android:title="Add CC"
android:orderInCategory="95"
app:showAsAction="always" />
</menu>

View File

@@ -375,12 +375,17 @@
<string name="security">Sicherheit</string>
<string name="entry_history">Verlauf</string>
<string name="entry_setup_otp">Einmalpasswort einrichten</string>
<string name="entry_setup_cc">Kreditkarte hinzufügen</string>
<string name="otp_type">OTP-Typ</string>
<string name="otp_secret">Geheimnis</string>
<string name="otp_period">Zeitraum (Sekunden)</string>
<string name="otp_counter">Zähler</string>
<string name="otp_digits">Stellen</string>
<string name="otp_algorithm">Algorithmus</string>
<string name="cc_cardholder">Karteninhaber</string>
<string name="cc_number">Kreditkartennummer</string>
<string name="cc_expiration">Gültig bis</string>
<string name="cc_security_code">Prüfnummer</string>
<string name="entry_otp">OTP</string>
<string name="error_invalid_OTP">Ungültiges OTP-Geheimnis.</string>
<string name="error_disallow_no_credentials">Mindestens eine Anmeldeinformation muss festgelegt sein.</string>

View File

@@ -93,12 +93,17 @@
<string name="save">Save</string>
<string name="entry_title">Title</string>
<string name="entry_setup_otp">Set up one-time password</string>
<string name="entry_setup_cc">Edit credit card details</string>
<string name="otp_type">OTP type</string>
<string name="otp_secret">Secret</string>
<string name="otp_period">Period (seconds)</string>
<string name="otp_counter">Counter</string>
<string name="otp_digits">Digits</string>
<string name="otp_algorithm">Algorithm</string>
<string name="cc_cardholder">Cardholder</string>
<string name="cc_number">Credit Card Number</string>
<string name="cc_expiration">Expiration Date</string>
<string name="cc_security_code">CVV</string>
<string name="entry_otp">OTP</string>
<string name="entry_url">URL</string>
<string name="entry_user_name">Username</string>
@@ -525,6 +530,32 @@
<string name="unit_kibibyte">KiB</string>
<string name="unit_mebibyte">MiB</string>
<string name="unit_gibibyte">GiB</string>
<string-array name="month_array">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
</string-array>
<string-array name="year_array">
<item>20</item>
<item>21</item>
<item>22</item>
<item>23</item>
<item>24</item>
<item>25</item>
<item>26</item>
<item>27</item>
<item>28</item>
<item>29</item>
</string-array>
<string-array name="timeout_options">
<item>5 seconds</item>
<item>10 seconds</item>