mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Manage and autofill credit card details
This commit is contained in:
@@ -48,6 +48,7 @@ import com.kunzisoft.keepass.database.element.Entry
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.model.CreditCardCustomFields
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
@@ -316,6 +317,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
|
entryContentsView?.clearCreditCardFields()
|
||||||
entryInfo.customFields.forEach { field ->
|
entryInfo.customFields.forEach { field ->
|
||||||
val label = field.name
|
val label = field.name
|
||||||
// OTP field is already managed in dedicated view
|
// OTP field is already managed in dedicated view
|
||||||
@@ -326,7 +328,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
value.toString(),
|
value.toString(),
|
||||||
getString(R.string.copy_field, label)
|
getString(R.string.copy_field, CreditCardCustomFields.getLocalizedName(applicationContext, field.name))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -340,6 +342,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
@@ -433,15 +436,15 @@ class EntryActivity : LockingActivity() {
|
|||||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
entryFieldCopyView,
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
val appNameString = getString(R.string.app_name)
|
val appNameString = getString(R.string.app_name)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
getString(R.string.copy_field, appNameString))
|
getString(R.string.copy_field, appNameString))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.model.CreditCard
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
@@ -81,6 +82,7 @@ import kotlin.collections.ArrayList
|
|||||||
class EntryEditActivity : LockingActivity(),
|
class EntryEditActivity : LockingActivity(),
|
||||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||||
|
CreditCardDetailsDialogFragment.EntryCCFieldListener,
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
DatePickerDialog.OnDateSetListener,
|
DatePickerDialog.OnDateSetListener,
|
||||||
TimePickerDialog.OnTimeSetListener,
|
TimePickerDialog.OnTimeSetListener,
|
||||||
@@ -405,6 +407,11 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
|
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addNewCreditCard() {
|
||||||
|
val cc = CreditCard(entryEditFragment?.getExtraFields())
|
||||||
|
CreditCardDetailsDialogFragment.build(cc).show(supportFragmentManager, "CreditCardDialog")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new customized field
|
* Add a new customized field
|
||||||
*/
|
*/
|
||||||
@@ -455,6 +462,12 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
entryEditFragment?.removeExtraField(oldField)
|
entryEditFragment?.removeExtraField(oldField)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewCCFieldsApproved(ccFields: ArrayList<Field>) {
|
||||||
|
for (field in ccFields) {
|
||||||
|
entryEditFragment?.putExtraField(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new attachment
|
* Add a new attachment
|
||||||
*/
|
*/
|
||||||
@@ -609,8 +622,14 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
|
||||||
|
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
||||||
|
|
||||||
menu?.findItem(R.id.menu_add_field)?.apply {
|
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
|
isEnabled = allowCustomField
|
||||||
isVisible = allowCustomField
|
isVisible = allowCustomField
|
||||||
}
|
}
|
||||||
@@ -682,6 +701,10 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
addNewCustomField()
|
addNewCustomField()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.menu_add_cc -> {
|
||||||
|
addNewCreditCard()
|
||||||
|
return true
|
||||||
|
}
|
||||||
R.id.menu_add_attachment -> {
|
R.id.menu_add_attachment -> {
|
||||||
addNewAttachment(item)
|
addNewAttachment(item)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
|||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.model.CreditCardCustomFields
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.ExpirationView
|
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.collapse
|
||||||
import com.kunzisoft.keepass.view.expand
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
class EntryEditFragment: StylishFragment() {
|
class EntryEditFragment : StylishFragment() {
|
||||||
|
|
||||||
private lateinit var entryTitleLayoutView: TextInputLayout
|
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||||
private lateinit var entryTitleView: EditText
|
private lateinit var entryTitleView: EditText
|
||||||
@@ -91,7 +92,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
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())
|
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||||
|
|
||||||
@@ -152,7 +153,8 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
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()
|
populateViewsWithEntry()
|
||||||
@@ -185,7 +187,8 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
(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 mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||||
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
private var mOnEditButtonClickListener: ((item: Field) -> Unit)? = null
|
||||||
|
|
||||||
private fun buildViewFromField(extraField: Field): View? {
|
private fun buildViewFromField(extraField: Field): View? {
|
||||||
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
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)
|
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||||
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
|
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
|
||||||
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
|
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
|
extraFieldValueContainer?.id = View.NO_ID
|
||||||
|
|
||||||
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
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
|
* Remove all children and add new views for each field
|
||||||
*/
|
*/
|
||||||
fun assignExtraFields(fields: List<Field>,
|
fun assignExtraFields(fields: List<Field>,
|
||||||
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
onEditButtonClickListener: ((item: Field) -> Unit)?) {
|
||||||
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||||
// Reinit focused field
|
// Reinit focused field
|
||||||
mExtraFieldsList.clear()
|
mExtraFieldsList.clear()
|
||||||
mExtraFieldsList.addAll(fields)
|
mExtraFieldsList.addAll(fields)
|
||||||
extraFieldsListView.removeAllViews()
|
extraFieldsListView.removeAllViews()
|
||||||
|
|
||||||
|
|
||||||
fields.forEach {
|
fields.forEach {
|
||||||
extraFieldsListView.addView(buildViewFromField(it))
|
extraFieldsListView.addView(buildViewFromField(it))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request last focus
|
// Request last focus
|
||||||
mLastFocusedEditField?.let { focusField ->
|
mLastFocusedEditField?.let { focusField ->
|
||||||
mExtraViewToRequestFocus?.apply {
|
mExtraViewToRequestFocus?.apply {
|
||||||
requestFocus()
|
requestFocus()
|
||||||
setSelection(focusField.cursorSelectionStart,
|
setSelection(focusField.cursorSelectionStart,
|
||||||
focusField.cursorSelectionEnd)
|
focusField.cursorSelectionEnd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mLastFocusedEditField = null
|
mLastFocusedEditField = null
|
||||||
@@ -457,7 +471,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
|
|
||||||
fun assignAttachments(attachments: List<Attachment>,
|
fun assignAttachments(attachments: List<Attachment>,
|
||||||
streamDirection: StreamDirection,
|
streamDirection: StreamDirection,
|
||||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
onDeleteItem: (attachment: Attachment) -> Unit) {
|
||||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||||
@@ -474,7 +488,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun putAttachment(attachment: EntryAttachmentState,
|
fun putAttachment(attachment: EntryAttachmentState,
|
||||||
onPreviewLoaded: (()-> Unit)? = null) {
|
onPreviewLoaded: (() -> Unit)? = null) {
|
||||||
attachmentsContainerView.visibility = View.VISIBLE
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
attachmentsAdapter.putItem(attachment)
|
attachmentsAdapter.putItem(attachment)
|
||||||
attachmentsAdapter.onBinaryPreviewLoaded = {
|
attachmentsAdapter.onBinaryPreviewLoaded = {
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.model.CreditCardCustomFields
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -103,9 +106,9 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDataset(context: Context,
|
private fun buildDataset(context: Context,
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result,
|
struct: StructureParser.Result,
|
||||||
inlinePresentation: InlinePresentation?): Dataset? {
|
inlinePresentation: InlinePresentation?): Dataset? {
|
||||||
val title = makeEntryTitle(entryInfo)
|
val title = makeEntryTitle(entryInfo)
|
||||||
val views = newRemoteViews(context, title, entryInfo.icon)
|
val views = newRemoteViews(context, title, entryInfo.icon)
|
||||||
val builder = Dataset.Builder(views)
|
val builder = Dataset.Builder(views)
|
||||||
@@ -114,8 +117,89 @@ object AutofillHelper {
|
|||||||
struct.usernameId?.let { usernameId ->
|
struct.usernameId?.let { usernameId ->
|
||||||
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||||
}
|
}
|
||||||
struct.passwordId?.let { password ->
|
struct.passwordId?.let { passwordId ->
|
||||||
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
@@ -126,8 +210,8 @@ object AutofillHelper {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
builder.build()
|
builder.build()
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: Exception) {
|
||||||
// if not value be set
|
// at least one value must be set
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +240,7 @@ object AutofillHelper {
|
|||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
|
|
||||||
if (positionItem <= maxSuggestion-1
|
if (positionItem <= maxSuggestion - 1
|
||||||
&& inlinePresentationSpecs.size > positionItem) {
|
&& inlinePresentationSpecs.size > positionItem) {
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||||
|
|
||||||
@@ -191,7 +275,7 @@ object AutofillHelper {
|
|||||||
fun buildResponse(context: Context,
|
fun buildResponse(context: Context,
|
||||||
entriesInfo: List<EntryInfo>,
|
entriesInfo: List<EntryInfo>,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
|
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
// Add Header
|
// Add Header
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -208,6 +292,7 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add inline suggestion for new IME and dataset
|
// Add inline suggestion for new IME and dataset
|
||||||
entriesInfo.forEachIndexed { index, entryInfo ->
|
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||||
val inlinePresentation = inlineSuggestionsRequest?.let {
|
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||||
@@ -217,9 +302,17 @@ object AutofillHelper {
|
|||||||
null
|
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ class KeeAutofillService : AutofillService() {
|
|||||||
callback)
|
callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// else {
|
||||||
|
// TODO: Disable autofill for the app for API level >= 28
|
||||||
|
// public FillResponse.Builder disableAutofill (long duration)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ package com.kunzisoft.keepass.autofill
|
|||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,9 +37,7 @@ import java.util.*
|
|||||||
class StructureParser(private val structure: AssistStructure) {
|
class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
|
|
||||||
private var usernameNeeded = true
|
private var usernameIdCandidate: AutofillId? = null
|
||||||
|
|
||||||
private var usernameCandidate: AutofillId? = null
|
|
||||||
private var usernameValueCandidate: AutofillValue? = null
|
private var usernameValueCandidate: AutofillValue? = null
|
||||||
|
|
||||||
fun parse(saveValue: Boolean = false): Result? {
|
fun parse(saveValue: Boolean = false): Result? {
|
||||||
@@ -46,7 +45,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
result = Result()
|
result = Result()
|
||||||
result?.apply {
|
result?.apply {
|
||||||
allowSaveValues = saveValue
|
allowSaveValues = saveValue
|
||||||
usernameCandidate = null
|
usernameIdCandidate = null
|
||||||
usernameValueCandidate = null
|
usernameValueCandidate = null
|
||||||
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||||
val windowNode = structure.getWindowNodeAt(i)
|
val windowNode = structure.getWindowNodeAt(i)
|
||||||
@@ -57,26 +56,26 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
break@mainLoop
|
break@mainLoop
|
||||||
}
|
}
|
||||||
// If not explicit username field found, add the field just before password field.
|
// If not explicit username field found, add the field just before password field.
|
||||||
if (usernameId == null && passwordId != null && usernameCandidate != null) {
|
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
|
||||||
usernameId = usernameCandidate
|
usernameId = usernameIdCandidate
|
||||||
if (allowSaveValues) {
|
if (allowSaveValues) {
|
||||||
usernameValue = usernameValueCandidate
|
usernameValue = usernameValueCandidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the result only if password field is retrieved
|
return result
|
||||||
return if ((!usernameNeeded || result?.usernameId != null)
|
|
||||||
&& result?.passwordId != null)
|
|
||||||
result
|
|
||||||
else
|
|
||||||
null
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
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
|
// Get the domain of a web app
|
||||||
node.webDomain?.let { webDomain ->
|
node.webDomain?.let { webDomain ->
|
||||||
if (webDomain.isNotEmpty()) {
|
if (webDomain.isNotEmpty()) {
|
||||||
@@ -97,8 +96,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
var returnValue = false
|
var returnValue = false
|
||||||
// Only parse visible nodes
|
// Only parse visible nodes
|
||||||
if (node.visibility == View.VISIBLE) {
|
if (node.visibility == View.VISIBLE) {
|
||||||
if (node.autofillId != null
|
if (node.autofillId != null) {
|
||||||
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
|
||||||
// Parse methods
|
// Parse methods
|
||||||
val hints = node.autofillHints
|
val hints = node.autofillHints
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
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_USERNAME, true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|| it.contains("email", true)
|
|| it.contains("email", true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username hint")
|
Log.d(TAG, "Autofill username hint")
|
||||||
@@ -139,14 +137,70 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password hint")
|
Log.d(TAG, "Autofill password hint")
|
||||||
// Username not needed in this case
|
return true
|
||||||
usernameNeeded = false
|
}
|
||||||
|
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
|
return true
|
||||||
}
|
}
|
||||||
// Ignore autocomplete="off"
|
// Ignore autocomplete="off"
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
||||||
it.equals("off", true) ||
|
it.equals("off", true) ||
|
||||||
it.equals("on", true) -> {
|
it.equals("on", true) -> {
|
||||||
Log.d(TAG, "Autofill web hint")
|
Log.d(TAG, "Autofill web hint")
|
||||||
return parseNodeByHtmlAttributes(node)
|
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}")
|
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
}
|
}
|
||||||
"text" -> {
|
"text" -> {
|
||||||
usernameCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
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_NORMAL,
|
||||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
usernameCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
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?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||||
usernameNeeded = false
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
@@ -252,16 +305,15 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
when {
|
when {
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
usernameCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
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,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||||
usernameNeeded = false
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -275,6 +327,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class Result {
|
class Result {
|
||||||
|
var isWebView: Boolean = false
|
||||||
var applicationId: String? = null
|
var applicationId: String? = null
|
||||||
|
|
||||||
var webDomain: String? = null
|
var webDomain: String? = null
|
||||||
@@ -289,6 +342,11 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
field = value
|
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
|
var usernameId: AutofillId? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field == null)
|
if (field == null)
|
||||||
@@ -301,6 +359,42 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
field = value
|
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> {
|
fun allAutofillIds(): Array<AutofillId> {
|
||||||
val all = ArrayList<AutofillId>()
|
val all = ArrayList<AutofillId>()
|
||||||
usernameId?.let {
|
usernameId?.let {
|
||||||
@@ -309,6 +403,24 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
passwordId?.let {
|
passwordId?.let {
|
||||||
all.add(it)
|
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()
|
return all.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +438,41 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
if (allowSaveValues && field == null)
|
if (allowSaveValues && field == null)
|
||||||
field = value
|
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 {
|
companion object {
|
||||||
|
|||||||
94
app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt
Normal file
94
app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|||||||
import com.kunzisoft.keepass.database.search.UuidUtil
|
import com.kunzisoft.keepass.database.search.UuidUtil
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.model.CreditCardCustomFields
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -55,6 +56,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
private var fontInVisibility: Boolean = false
|
private var fontInVisibility: Boolean = false
|
||||||
|
|
||||||
|
private val entryFieldsContainerView: View
|
||||||
|
|
||||||
private val userNameFieldView: EntryField
|
private val userNameFieldView: EntryField
|
||||||
private val passwordFieldView: EntryField
|
private val passwordFieldView: EntryField
|
||||||
private val otpFieldView: EntryField
|
private val otpFieldView: EntryField
|
||||||
@@ -66,6 +69,9 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
private val extraFieldsContainerView: View
|
private val extraFieldsContainerView: View
|
||||||
private val extraFieldsListView: ViewGroup
|
private val extraFieldsListView: ViewGroup
|
||||||
|
|
||||||
|
private val creditCardContainerView: View
|
||||||
|
private val creditCardFieldsListView: ViewGroup
|
||||||
|
|
||||||
private val creationDateView: TextView
|
private val creationDateView: TextView
|
||||||
private val modificationDateView: TextView
|
private val modificationDateView: TextView
|
||||||
private val expiresImageView: ImageView
|
private val expiresImageView: ImageView
|
||||||
@@ -87,6 +93,9 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater?.inflate(R.layout.view_entry_contents, this)
|
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 = findViewById(R.id.entry_user_name_field)
|
||||||
userNameFieldView.setLabel(R.string.entry_user_name)
|
userNameFieldView.setLabel(R.string.entry_user_name)
|
||||||
|
|
||||||
@@ -107,6 +116,9 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
|
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
|
||||||
extraFieldsListView = findViewById(R.id.extra_fields_list)
|
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)
|
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
|
||||||
attachmentsListView = findViewById(R.id.entry_attachments_list)
|
attachmentsListView = findViewById(R.id.entry_attachments_list)
|
||||||
attachmentsListView?.apply {
|
attachmentsListView?.apply {
|
||||||
@@ -157,6 +169,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
setValue(userName)
|
setValue(userName)
|
||||||
applyFontVisibility(fontInVisibility)
|
applyFontVisibility(fontInVisibility)
|
||||||
|
showOrHideEntryFieldsContainer(false)
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -173,7 +186,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
setValue(password, true)
|
setValue(password, true)
|
||||||
applyFontVisibility(fontInVisibility)
|
applyFontVisibility(fontInVisibility)
|
||||||
activateCopyButton(allowCopyPassword)
|
activateCopyButton(allowCopyPassword)
|
||||||
}else {
|
showOrHideEntryFieldsContainer(false)
|
||||||
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
assignCopyButtonClickListener(onClickListener)
|
assignCopyButtonClickListener(onClickListener)
|
||||||
@@ -220,6 +234,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
showOrHideEntryFieldsContainer(false)
|
||||||
} else {
|
} else {
|
||||||
otpFieldView.visibility = View.GONE
|
otpFieldView.visibility = View.GONE
|
||||||
otpProgressView?.visibility = View.GONE
|
otpProgressView?.visibility = View.GONE
|
||||||
@@ -231,6 +246,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
if (url != null && url.isNotEmpty()) {
|
if (url != null && url.isNotEmpty()) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
setValue(url)
|
setValue(url)
|
||||||
|
showOrHideEntryFieldsContainer(false)
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -243,6 +259,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
setValue(notes)
|
setValue(notes)
|
||||||
applyFontVisibility(fontInVisibility)
|
applyFontVisibility(fontInVisibility)
|
||||||
|
showOrHideEntryFieldsContainer(false)
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
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
|
* Extra Fields
|
||||||
* -------------
|
* -------------
|
||||||
@@ -292,11 +313,19 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
extraFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
|
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,
|
fun addExtraField(title: String,
|
||||||
value: ProtectedString,
|
value: ProtectedString,
|
||||||
allowCopy: Boolean,
|
allowCopy: Boolean,
|
||||||
onCopyButtonClickListener: OnClickListener?) {
|
onCopyButtonClickListener: OnClickListener?) {
|
||||||
|
|
||||||
|
if (title in CreditCardCustomFields.CC_CUSTOM_FIELDS) {
|
||||||
|
return addExtraCCField(title, value, allowCopy, onCopyButtonClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
val entryCustomField: EntryField? = EntryField(context)
|
val entryCustomField: EntryField? = EntryField(context)
|
||||||
entryCustomField?.apply {
|
entryCustomField?.apply {
|
||||||
setLabel(title)
|
setLabel(title)
|
||||||
@@ -306,17 +335,48 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
assignCopyButtonClickListener(onCopyButtonClickListener)
|
assignCopyButtonClickListener(onCopyButtonClickListener)
|
||||||
applyFontVisibility(fontInVisibility)
|
applyFontVisibility(fontInVisibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
entryCustomField?.let {
|
entryCustomField?.let {
|
||||||
extraFieldsListView.addView(it)
|
extraFieldsListView.addView(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
showOrHideExtraFieldsContainer(false)
|
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() {
|
fun clearExtraFields() {
|
||||||
extraFieldsListView.removeAllViews()
|
extraFieldsListView.removeAllViews()
|
||||||
showOrHideExtraFieldsContainer(true)
|
showOrHideExtraFieldsContainer(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearCreditCardFields() {
|
||||||
|
creditCardFieldsListView.removeAllViews()
|
||||||
|
showOrHideCreditCardContainer(true)
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------
|
/* -------------
|
||||||
* Attachments
|
* Attachments
|
||||||
* -------------
|
* -------------
|
||||||
@@ -332,7 +392,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
fun assignAttachments(attachments: Set<Attachment>,
|
fun assignAttachments(attachments: Set<Attachment>,
|
||||||
streamDirection: StreamDirection,
|
streamDirection: StreamDirection,
|
||||||
onAttachmentClicked: (attachment: Attachment)->Unit) {
|
onAttachmentClicked: (attachment: Attachment) -> Unit) {
|
||||||
showAttachments(attachments.isNotEmpty())
|
showAttachments(attachments.isNotEmpty())
|
||||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||||
attachmentsAdapter.onItemClickListener = { item ->
|
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.clear()
|
||||||
historyAdapter.entryHistoryList.addAll(history)
|
historyAdapter.entryHistoryList.addAll(history)
|
||||||
historyAdapter.onItemClickListener = { item, position ->
|
historyAdapter.onItemClickListener = { item, position ->
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.view
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -31,6 +32,7 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.core.text.util.LinkifyCompat
|
import androidx.core.text.util.LinkifyCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
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.model.CreditCardCustomFields
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class EntryField @JvmOverloads constructor(context: Context,
|
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() {
|
fun setAutoLink() {
|
||||||
if (!isProtected) linkify()
|
if (!isProtected) linkify()
|
||||||
changeProtectedValueParameters()
|
changeProtectedValueParameters()
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
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() {
|
fun TextView.applyFontVisibility() {
|
||||||
val typeFace = Typeface.createFromAsset(context.assets, "fonts/FiraMono-Regular.ttf")
|
val typeFace = Typeface.createFromAsset(context.assets, "fonts/FiraMono-Regular.ttf")
|
||||||
|
|||||||
10
app/src/main/res/drawable/ic_baseline_credit_card_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_credit_card_24.xml
Normal 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>
|
||||||
@@ -83,6 +83,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/validate"
|
android:contentDescription="@string/validate"
|
||||||
|
android:layout_marginBottom="?attr/actionBarSize"
|
||||||
android:src="@drawable/ic_check_white_24dp"
|
android:src="@drawable/ic_check_white_24dp"
|
||||||
app:fabSize="mini"
|
app:fabSize="mini"
|
||||||
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
|
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
|
||||||
|
|||||||
154
app/src/main/res/layout/entry_cc_details_dialog.xml
Normal file
154
app/src/main/res/layout/entry_cc_details_dialog.xml
Normal 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>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/entry_fields_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/card_view_margin"
|
android:layout_marginTop="@dimen/card_view_margin"
|
||||||
@@ -96,6 +97,25 @@
|
|||||||
android:orientation="vertical" />
|
android:orientation="vertical" />
|
||||||
</androidx.cardview.widget.CardView>
|
</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
|
<androidx.cardview.widget.CardView
|
||||||
android:id="@+id/entry_attachments_container"
|
android:id="@+id/entry_attachments_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -39,4 +39,9 @@
|
|||||||
android:orderInCategory="94"
|
android:orderInCategory="94"
|
||||||
app:iconTint="?attr/colorControlNormal"
|
app:iconTint="?attr/colorControlNormal"
|
||||||
app:showAsAction="always" />
|
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>
|
</menu>
|
||||||
|
|||||||
@@ -375,12 +375,17 @@
|
|||||||
<string name="security">Sicherheit</string>
|
<string name="security">Sicherheit</string>
|
||||||
<string name="entry_history">Verlauf</string>
|
<string name="entry_history">Verlauf</string>
|
||||||
<string name="entry_setup_otp">Einmalpasswort einrichten</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_type">OTP-Typ</string>
|
||||||
<string name="otp_secret">Geheimnis</string>
|
<string name="otp_secret">Geheimnis</string>
|
||||||
<string name="otp_period">Zeitraum (Sekunden)</string>
|
<string name="otp_period">Zeitraum (Sekunden)</string>
|
||||||
<string name="otp_counter">Zähler</string>
|
<string name="otp_counter">Zähler</string>
|
||||||
<string name="otp_digits">Stellen</string>
|
<string name="otp_digits">Stellen</string>
|
||||||
<string name="otp_algorithm">Algorithmus</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="entry_otp">OTP</string>
|
||||||
<string name="error_invalid_OTP">Ungültiges OTP-Geheimnis.</string>
|
<string name="error_invalid_OTP">Ungültiges OTP-Geheimnis.</string>
|
||||||
<string name="error_disallow_no_credentials">Mindestens eine Anmeldeinformation muss festgelegt sein.</string>
|
<string name="error_disallow_no_credentials">Mindestens eine Anmeldeinformation muss festgelegt sein.</string>
|
||||||
|
|||||||
@@ -93,12 +93,17 @@
|
|||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="entry_title">Title</string>
|
<string name="entry_title">Title</string>
|
||||||
<string name="entry_setup_otp">Set up one-time password</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_type">OTP type</string>
|
||||||
<string name="otp_secret">Secret</string>
|
<string name="otp_secret">Secret</string>
|
||||||
<string name="otp_period">Period (seconds)</string>
|
<string name="otp_period">Period (seconds)</string>
|
||||||
<string name="otp_counter">Counter</string>
|
<string name="otp_counter">Counter</string>
|
||||||
<string name="otp_digits">Digits</string>
|
<string name="otp_digits">Digits</string>
|
||||||
<string name="otp_algorithm">Algorithm</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_otp">OTP</string>
|
||||||
<string name="entry_url">URL</string>
|
<string name="entry_url">URL</string>
|
||||||
<string name="entry_user_name">Username</string>
|
<string name="entry_user_name">Username</string>
|
||||||
@@ -525,6 +530,32 @@
|
|||||||
<string name="unit_kibibyte">KiB</string>
|
<string name="unit_kibibyte">KiB</string>
|
||||||
<string name="unit_mebibyte">MiB</string>
|
<string name="unit_mebibyte">MiB</string>
|
||||||
<string name="unit_gibibyte">GiB</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">
|
<string-array name="timeout_options">
|
||||||
<item>5 seconds</item>
|
<item>5 seconds</item>
|
||||||
<item>10 seconds</item>
|
<item>10 seconds</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user