diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index 87b9de595..b655a4291 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -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(R.id.menu_edit) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index d5d2dd9f8..cc31ac0b3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -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) { + 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 diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/CreditCardDetailsDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/CreditCardDetailsDialogFragment.kt new file mode 100644 index 000000000..8e09fc2a1 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/CreditCardDetailsDialogFragment.kt @@ -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) + } + + 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) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt index 2c8f1a4f8..46f5b710b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt @@ -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 = 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, - 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, 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 = { diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt index db75561ed..23aac0173 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt @@ -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, 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() } /** diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt index 8f093e18d..b41e65981 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt @@ -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) + // } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt index c93cb4a36..d92f21400 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -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? = null + var ccExpYearOptions: Array? = 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 { val all = ArrayList() 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 { diff --git a/app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt b/app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt new file mode 100644 index 000000000..49a368a3c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt @@ -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?) : 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 { + override fun createFromParcel(parcel: Parcel): CreditCard { + return CreditCard(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/model/CreditCardCustomFields.kt b/app/src/main/java/com/kunzisoft/keepass/model/CreditCardCustomFields.kt new file mode 100644 index 000000000..2dbdeb352 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/CreditCardCustomFields.kt @@ -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 { + + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt index 41d13788f..eec18660e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt @@ -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, 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, action: (historyItem: Entry, position: Int)->Unit) { + fun assignHistory(history: ArrayList, action: (historyItem: Entry, position: Int) -> Unit) { historyAdapter.clear() historyAdapter.entryHistoryList.addAll(history) historyAdapter.onItemClickListener = { item, position -> diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt index 1c49ec078..89bfb2291 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt @@ -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() diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index df734a796..af8725ed2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -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") diff --git a/app/src/main/res/drawable/ic_baseline_credit_card_24.xml b/app/src/main/res/drawable/ic_baseline_credit_card_24.xml new file mode 100644 index 000000000..9c0fb1f1e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_credit_card_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_entry_edit.xml b/app/src/main/res/layout/activity_entry_edit.xml index f5faa0949..703610489 100644 --- a/app/src/main/res/layout/activity_entry_edit.xml +++ b/app/src/main/res/layout/activity_entry_edit.xml @@ -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" diff --git a/app/src/main/res/layout/entry_cc_details_dialog.xml b/app/src/main/res/layout/entry_cc_details_dialog.xml new file mode 100644 index 000000000..28ed1d46a --- /dev/null +++ b/app/src/main/res/layout/entry_cc_details_dialog.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_entry_contents.xml b/app/src/main/res/layout/view_entry_contents.xml index 48164dfa5..81c908cd4 100644 --- a/app/src/main/res/layout/view_entry_contents.xml +++ b/app/src/main/res/layout/view_entry_contents.xml @@ -25,6 +25,7 @@ android:orientation="vertical"> + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 89fd2907b..c73cae75e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -375,12 +375,17 @@ Sicherheit Verlauf Einmalpasswort einrichten + Kreditkarte hinzufügen OTP-Typ Geheimnis Zeitraum (Sekunden) Zähler Stellen Algorithmus + Karteninhaber + Kreditkartennummer + Gültig bis + Prüfnummer OTP Ungültiges OTP-Geheimnis. Mindestens eine Anmeldeinformation muss festgelegt sein. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a12c2ef0e..68b9873c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,12 +93,17 @@ Save Title Set up one-time password + Edit credit card details OTP type Secret Period (seconds) Counter Digits Algorithm + Cardholder + Credit Card Number + Expiration Date + CVV OTP URL Username @@ -525,6 +530,32 @@ KiB MiB GiB + + 01 + 02 + 03 + 04 + 05 + 06 + 07 + 08 + 09 + 10 + 11 + 12 + + + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 5 seconds 10 seconds