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.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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.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 = {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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.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 ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
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_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"
|
||||
|
||||
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">
|
||||
|
||||
<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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user