Let user save credit card details after filling out new form

This commit is contained in:
Ulrich Dürholz
2021-04-18 14:28:38 +02:00
parent 2ba8702787
commit 80c4ba6723
12 changed files with 265 additions and 280 deletions

View File

@@ -191,16 +191,15 @@ class EntryEditActivity : LockingActivity(),
val registerInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent) val registerInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
val searchInfo: SearchInfo? = registerInfo?.searchInfo val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) ?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
registerInfo?.username?.let {
tempEntryInfo?.username = it
}
registerInfo?.password?.let {
tempEntryInfo?.password = it
}
searchInfo?.let { tempSearchInfo -> searchInfo?.let { tempSearchInfo ->
tempEntryInfo?.saveSearchInfo(mDatabase, tempSearchInfo) tempEntryInfo?.saveSearchInfo(mDatabase, tempSearchInfo)
} }
registerInfo?.let { regInfo ->
tempEntryInfo?.saveRegisterInfo(mDatabase, regInfo)
}
// Build fragment to manage entry modification // Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment? entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) { if (entryEditFragment == null) {
@@ -408,7 +407,25 @@ class EntryEditActivity : LockingActivity(),
} }
private fun addNewCreditCard() { private fun addNewCreditCard() {
val cc = CreditCard(entryEditFragment?.getExtraFields()) var cardholder: String? = null
var number: String? = null
var expiration: String? = null
var cvv: String? = null
entryEditFragment?.getExtraFields()?.forEach() { field ->
when (field.name) {
CreditCardCustomFields.CC_CARDHOLDER_FIELD_NAME ->
cardholder = field.protectedValue.stringValue
CreditCardCustomFields.CC_NUMBER_FIELD_NAME ->
number = field.protectedValue.stringValue
CreditCardCustomFields.CC_EXP_FIELD_NAME ->
expiration = field.protectedValue.stringValue
CreditCardCustomFields.CC_CVV_FIELD_NAME ->
cvv = field.protectedValue.stringValue
}
}
val cc = CreditCard(cardholder, number, expiration, cvv)
CreditCardDetailsDialogFragment.build(cc).show(supportFragmentManager, "CreditCardDialog") CreditCardDetailsDialogFragment.build(cc).show(supportFragmentManager, "CreditCardDialog")
} }

View File

@@ -3,8 +3,6 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.WindowManager import android.view.WindowManager
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
@@ -13,9 +11,10 @@ import android.widget.Spinner
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.CreditCardCustomFields.buildAllFields
import com.kunzisoft.keepass.model.CreditCard import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.CreditCardCustomFields.buildAllFields
import com.kunzisoft.keepass.model.Field
import java.util.*
class CreditCardDetailsDialogFragment : DialogFragment() { class CreditCardDetailsDialogFragment : DialogFragment() {
private var mCreditCard: CreditCard? = null private var mCreditCard: CreditCard? = null
@@ -28,9 +27,6 @@ class CreditCardDetailsDialogFragment : DialogFragment() {
private var mCcExpirationMonthSpinner: Spinner? = null private var mCcExpirationMonthSpinner: Spinner? = null
private var mCcExpirationYearSpinner: Spinner? = null private var mCcExpirationYearSpinner: Spinner? = null
private var mCcCardNumberWellFormed: Boolean = false
private var mCcSecurityCodeWellFormed: Boolean = false
private var mPositiveButton: Button? = null private var mPositiveButton: Button? = null
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
@@ -57,8 +53,6 @@ class CreditCardDetailsDialogFragment : DialogFragment() {
if (d != null) { if (d != null) {
mPositiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button mPositiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
mPositiveButton?.run { mPositiveButton?.run {
isEnabled = mCcSecurityCodeWellFormed && mCcCardNumberWellFormed
attachListeners()
setOnClickListener { setOnClickListener {
submitDialog() submitDialog()
} }
@@ -75,7 +69,7 @@ class CreditCardDetailsDialogFragment : DialogFragment() {
val ccNumber = mCcCardNumber?.text?.toString() ?: "" val ccNumber = mCcCardNumber?.text?.toString() ?: ""
val month = mCcExpirationMonthSpinner?.selectedItem?.toString() ?: "" val month = mCcExpirationMonthSpinner?.selectedItem?.toString() ?: ""
val year = mCcExpirationYearSpinner?.selectedItem?.toString() ?: "" val year = mCcExpirationYearSpinner?.selectedItem?.toString()?.substring(2,4) ?: ""
val cvv = mCcSecurityCode?.text?.toString() ?: "" val cvv = mCcSecurityCode?.text?.toString() ?: ""
val ccName = mCcCardholderName?.text?.toString() ?: "" val ccName = mCcCardholderName?.text?.toString() ?: ""
@@ -106,37 +100,43 @@ class CreditCardDetailsDialogFragment : DialogFragment() {
activity?.let { activity -> activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.entry_cc_details_dialog, null) val root = activity.layoutInflater.inflate(R.layout.entry_cc_details_dialog, null)
mCcCardholderName = root?.findViewById(R.id.creditCardholderNameField) root?.run {
mCcCardholderName = findViewById(R.id.creditCardholderNameField)
mCcCardNumber = findViewById(R.id.creditCardNumberField)
mCcSecurityCode = findViewById(R.id.creditCardSecurityCode)
mCcExpirationMonthSpinner = findViewById(R.id.expirationMonth)
mCcExpirationYearSpinner = findViewById(R.id.expirationYear)
mCcExpirationMonthSpinner = root?.findViewById(R.id.expirationMonth) mCreditCard?.cardholder?.let {
mCcExpirationYearSpinner = root?.findViewById(R.id.expirationYear) mCcCardholderName?.setText(it)
}
mCcCardNumber = root?.findViewById(R.id.creditCardNumberField) mCreditCard?.number?.let {
mCcSecurityCode = root?.findViewById(R.id.creditCardSecurityCode) mCcCardNumber?.setText(it)
}
mCreditCard?.let { mCreditCard?.cvv?.let {
mCcCardholderName!!.setText(it.cardholder) mCcSecurityCode?.setText(it)
mCcCardNumberWellFormed = true }
mCcCardNumber!!.setText(it.number)
mCcSecurityCodeWellFormed = true
mCcSecurityCode!!.setText(it.cvv)
} }
val monthAdapter = ArrayAdapter.createFromResource(requireContext(), val months = arrayOf("01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12")
R.array.month_array, android.R.layout.simple_spinner_item) mCcExpirationMonthSpinner?.let { spinner ->
monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) spinner.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, months)
mCcExpirationMonthSpinner!!.adapter = monthAdapter mCreditCard?.let { cc ->
spinner.setSelection(getIndex(spinner, cc.getExpirationMonth()))
}
}
mCreditCard?.let { mCcExpirationMonthSpinner!!.setSelection( val years = arrayOfNulls<String>(5)
getIndex(mCcExpirationMonthSpinner!!, it.getExpirationMonth()) ) } val year = Calendar.getInstance()[Calendar.YEAR]
for (i in years.indices) {
val yearAdapter = ArrayAdapter.createFromResource(requireContext(), years[i] = (year + i).toString()
R.array.year_array, android.R.layout.simple_spinner_item) }
yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) mCcExpirationYearSpinner?.let { spinner ->
mCcExpirationYearSpinner!!.adapter = yearAdapter spinner.adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item, years)
mCreditCard?.let { cc ->
mCreditCard?.let { mCcExpirationYearSpinner!!.setSelection( spinner.setSelection(getIndex(spinner, "20" + cc.getExpirationYear()))
getIndex(mCcExpirationYearSpinner!!, it.getExpirationYear()) ) } }
}
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
@@ -155,44 +155,14 @@ class CreditCardDetailsDialogFragment : DialogFragment() {
private fun getIndex(spinner: Spinner, value: String?): Int { private fun getIndex(spinner: Spinner, value: String?): Int {
for (i in 0 until spinner.count) { for (i in 0 until spinner.count) {
if (spinner.getItemAtPosition(i).toString().equals(value, ignoreCase = true)) { if (spinner.getItemAtPosition(i).toString() == value) {
return i return i
} }
} }
return 0 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 { companion object {
private const val KEY_CREDIT_CARD = "KEY_CREDIT_CARD" private const val KEY_CREDIT_CARD = "KEY_CREDIT_CARD"
fun build(creditCard: CreditCard? = null): CreditCardDetailsDialogFragment { fun build(creditCard: CreditCard? = null): CreditCardDetailsDialogFragment {

View File

@@ -137,14 +137,15 @@ object AutofillHelper {
// of length four in the format MMYY // of length four in the format MMYY
if (field.protectedValue.stringValue.length != 4) continue if (field.protectedValue.stringValue.length != 4) continue
// get month (month in database entry is stored as String in the range 01..12) // get month (month in database entry is stored as String in the format MM)
val monthString = field.protectedValue.stringValue.substring(0, 2) val monthString = field.protectedValue.stringValue.substring(0, 2)
if (monthString !in context.resources.getStringArray(R.array.month_array)) continue val month = monthString.toIntOrNull() ?: 0
if (month < 1 || month > 12) continue
val month = monthString.toInt() // get year (year in database entry is stored as String in the format YY)
// get year (year in database entry is stored as String in the range 20..29)
val yearString = field.protectedValue.stringValue.substring(2, 4) val yearString = field.protectedValue.stringValue.substring(2, 4)
if (yearString !in context.resources.getStringArray(R.array.year_array)) continue val year = "20$yearString".toIntOrNull() ?: 0
if (year == 0) continue
struct.ccExpDateId?.let { struct.ccExpDateId?.let {
if (struct.isWebView) { if (struct.isWebView) {
@@ -154,7 +155,6 @@ object AutofillHelper {
} else { } else {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.clear() calendar.clear()
val year = "20$yearString".toInt()
calendar[Calendar.YEAR] = year calendar[Calendar.YEAR] = year
// Month value is 0-based. e.g., 0 for January // Month value is 0-based. e.g., 0 for January
calendar[Calendar.MONTH] = month - 1 calendar[Calendar.MONTH] = month - 1
@@ -164,34 +164,33 @@ object AutofillHelper {
} }
struct.ccExpDateMonthId?.let { struct.ccExpDateMonthId?.let {
if (struct.isWebView) { if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(month.toString())) builder.setValue(it, AutofillValue.forText(monthString))
} else { } else {
if (struct.ccExpMonthOptions != null) { if (struct.ccExpMonthOptions != null) {
// index starts at 0 // index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1)) builder.setValue(it, AutofillValue.forList(month - 1))
} else { } else {
builder.setValue(it, AutofillValue.forText(month.toString())) builder.setValue(it, AutofillValue.forText(monthString))
} }
} }
} }
struct.ccExpDateYearId?.let { struct.ccExpDateYearId?.let {
if (struct.isWebView) { var autofillValue: AutofillValue? = null
builder.setValue(it, AutofillValue.forText(yearString))
} else {
if (struct.ccExpYearOptions != null) {
var yearIndex = struct.ccExpYearOptions!!.indexOf(yearString)
if (yearIndex == -1) { struct.ccExpYearOptions?.let { options ->
yearIndex = struct.ccExpYearOptions!!.indexOf("20$yearString") var yearIndex = options.indexOf(yearString)
}
if (yearIndex != -1) { if (yearIndex == -1) {
builder.setValue(it, AutofillValue.forList(yearIndex)) yearIndex = options.indexOf("20$yearString")
} else {
builder.setValue(it, AutofillValue.forText(yearString))
}
} else {
builder.setValue(it, AutofillValue.forText(yearString))
} }
if (yearIndex != -1) {
autofillValue = AutofillValue.forList(yearIndex)
builder.setValue(it, autofillValue)
}
}
if (autofillValue == null) {
builder.setValue(it, AutofillValue.forText("20$yearString"))
} }
} }
} }

View File

@@ -38,6 +38,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
@@ -101,10 +102,15 @@ class KeeAutofillService : AutofillService() {
callback) callback)
} }
} }
// else { // TODO does it make sense to disable autofill here? how long?
// TODO: Disable autofill for the app for API level >= 28 // else {
// public FillResponse.Builder disableAutofill (long duration) // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// } // val builder = FillResponse.Builder()
// // disable for a while (duration in ms)
// builder.disableAutofill(5*60*1000)
// callback.onSuccess(builder.build())
// }
// }
} }
} }
} }
@@ -159,23 +165,40 @@ class KeeAutofillService : AutofillService() {
RemoteViews(packageName, R.layout.item_autofill_unlock) RemoteViews(packageName, R.layout.item_autofill_unlock)
} }
// Tell to service the interest to save credentials // Tell the autofill framework the interest to save credentials
if (askToSaveData) { if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val info = ArrayList<AutofillId>() val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Only if at least a password // Only if at least a password
parseResult.passwordId?.let { passwordInfo -> parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo -> parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
info.add(usernameInfo) requiredIds.add(usernameInfo)
} }
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
info.add(passwordInfo) requiredIds.add(passwordInfo)
} }
if (info.isNotEmpty()) { // or a credit card form
responseBuilder.setSaveInfo( if (requiredIds.isEmpty()) {
SaveInfo.Builder(types, info.toTypedArray()).build() parseResult.ccnId?.let { numberId ->
) types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
}
parseResult.ccExpDateId?.let { id -> optionalIds.add(id) }
parseResult.ccExpDateYearId?.let { id -> optionalIds.add(id) }
parseResult.ccExpDateMonthId?.let { id -> optionalIds.add(id) }
parseResult.ccNameId?.let { id -> optionalIds.add(id) }
parseResult.cvvId?.let { id -> optionalIds.add(id) }
}
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
}
responseBuilder.setSaveInfo(builder.build())
} }
} }
@@ -227,14 +250,27 @@ class KeeAutofillService : AutofillService() {
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) { && autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
Log.d(TAG, "autofill onSaveRequest password") Log.d(TAG, "autofill onSaveRequest password")
if (parseResult.ccExpirationValue == null) {
if (parseResult.ccExpDateMonthValue != 0 && parseResult.ccExpDateYearValue != 0) {
parseResult.ccExpirationValue = parseResult.ccExpDateMonthValue.toString().padStart(2, '0') + parseResult.ccExpDateYearValue.toString()
}
}
val creditCard = CreditCard(parseResult.ccName, parseResult.ccNumber,
parseResult.ccExpirationValue, parseResult.cvv)
// Show UI to save data // Show UI to save data
val registerInfo = RegisterInfo(SearchInfo().apply { val registerInfo = RegisterInfo(
applicationId = parseResult.applicationId SearchInfo().apply {
webDomain = parseResult.webDomain applicationId = parseResult.applicationId
webScheme = parseResult.webScheme webDomain = parseResult.webDomain
}, webScheme = parseResult.webScheme
},
parseResult.usernameValue?.textValue?.toString(), parseResult.usernameValue?.textValue?.toString(),
parseResult.passwordValue?.textValue?.toString()) parseResult.passwordValue?.textValue?.toString(),
creditCard)
// TODO Callback in each activity #765 // TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this, // callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,

View File

@@ -64,7 +64,10 @@ class StructureParser(private val structure: AssistStructure) {
} }
} }
return result return if (result?.passwordId != null || result?.ccnId != null)
result
else
null
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }
@@ -140,62 +143,88 @@ class StructureParser(private val structure: AssistStructure) {
return true return true
} }
it == "cc-name" -> { it == "cc-name" -> {
Log.d(TAG, "AUTOFILL cc-name hint")
result?.ccNameId = autofillId result?.ccNameId = autofillId
result?.ccNameValue = node.autofillValue result?.ccName = node.autofillValue?.textValue?.toString()
return true
} }
it == View.AUTOFILL_HINT_CREDIT_CARD_NUMBER || it == "cc-number" -> { 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") Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_NUMBER hint")
return true result?.ccnId = autofillId
result?.ccNumber = node.autofillValue?.textValue?.toString()
} }
it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE || it == "cc-exp" -> { // expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
it == "cc-exp" -> {
Log.d(TAG, "AUTOFILL cc-exp hint")
result?.ccExpDateId = autofillId result?.ccExpDateId = autofillId
node.autofillValue?.let { value ->
if (value.isText && value.textValue.length == 7) {
value.textValue.let { date ->
result?.ccExpirationValue = date.substring(5, 7) + date.substring(2, 4)
}
}
}
}
it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE -> {
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE hint") Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE hint")
return true result?.ccExpDateId = autofillId
node.autofillValue?.let { value ->
if (value.isDate) {
val calendar = Calendar.getInstance()
calendar.clear()
calendar.timeInMillis = value.dateValue
val year = calendar.get(Calendar.YEAR).toString().substring(2,4)
val month = calendar.get(Calendar.MONTH).inc().toString().padStart(2, '0')
result?.ccExpirationValue = month + year
}
}
} }
it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR || it == "cc-exp-year" -> { it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR || it == "cc-exp-year" -> {
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR hint") Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR hint")
result?.ccExpDateYearId = autofillId 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) { if (node.autofillOptions != null) {
result?.ccExpYearOptions = node.autofillOptions result?.ccExpYearOptions = node.autofillOptions
} }
return true node.autofillValue?.let { value ->
var year = 0
try {
if (value.isText) {
year = value.textValue.toString().toInt()
}
if (value.isList) {
year = node.autofillOptions?.get(value.listValue).toString().toInt()
}
} catch (e: Exception) {
year = 0
}
result?.ccExpDateYearValue = year % 100
}
} }
it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH || it == "cc-exp-month" -> { it == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH || it == "cc-exp-month" -> {
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH hint") Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH hint")
result?.ccExpDateMonthId = autofillId 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) { if (node.autofillOptions != null) {
result?.ccExpMonthOptions = node.autofillOptions result?.ccExpMonthOptions = node.autofillOptions
} }
return true node.autofillValue?.let { value ->
var month = 0
if (value.isText) {
try {
month = value.textValue.toString().toInt()
} catch (e: Exception) {
month = 0
}
}
if (value.isList) {
// assume list starts with January (index 0)
month = value.listValue + 1
}
result?.ccExpDateMonthValue = month
}
} }
it == View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE || it == "cc-csc" -> { 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") Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE hint")
return true result?.cvvId = autofillId
result?.cvv = node.autofillValue?.textValue?.toString()
} }
// Ignore autocomplete="off" // Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
@@ -409,15 +438,6 @@ class StructureParser(private val structure: AssistStructure) {
ccnId?.let { ccnId?.let {
all.add(it) all.add(it)
} }
ccExpDateId?.let {
all.add(it)
}
ccExpDateYearId?.let {
all.add(it)
}
ccExpDateMonthId?.let {
all.add(it)
}
cvvId?.let { cvvId?.let {
all.add(it) all.add(it)
} }
@@ -439,28 +459,33 @@ class StructureParser(private val structure: AssistStructure) {
field = value field = value
} }
// stores name of cardholder var ccName: String? = null
var ccNameValue: AutofillValue? = null
set(value) { set(value) {
if (allowSaveValues && field == null) if (allowSaveValues)
field = value field = value
} }
// stores credit card number var ccNumber: String? = null
var ccnValue: AutofillValue? = null
set(value) { set(value) {
if (allowSaveValues && field == null) if (allowSaveValues)
field = value field = value
} }
// for year of CC expiration date // format MMYY
var ccExpirationValue: String? = null
set(value) {
if (allowSaveValues)
field = value
}
// for year of CC expiration date: YY
var ccExpDateYearValue = 0 var ccExpDateYearValue = 0
set(value) { set(value) {
if (allowSaveValues) if (allowSaveValues)
field = value field = value
} }
// for month of CC expiration date // for month of CC expiration date: MM
var ccExpDateMonthValue = 0 var ccExpDateMonthValue = 0
set(value) { set(value) {
if (allowSaveValues) if (allowSaveValues)
@@ -468,9 +493,9 @@ class StructureParser(private val structure: AssistStructure) {
} }
// the security code for the credit card (also called CVV) // the security code for the credit card (also called CVV)
var cvvValue: AutofillValue? = null var cvv: String? = null
set(value) { set(value) {
if (allowSaveValues && field == null) if (allowSaveValues)
field = value field = value
} }
} }

View File

@@ -3,34 +3,14 @@ package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
class CreditCard() : Parcelable { data class CreditCard(val cardholder: String?, val number: String?,
var cardholder: String = ""; val expiration: String?, val cvv: String?) : Parcelable {
var number: String = "";
var expiration: String = "";
var cvv: String = "";
constructor(ccFields: List<Field>?) : this() { constructor(parcel: Parcel) : this(
ccFields?.let { parcel.readString(),
for (field in it) { parcel.readString(),
when (field.name) { parcel.readString(),
CreditCardCustomFields.CC_CARDHOLDER_FIELD_NAME -> parcel.readString()) {
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) { override fun writeToParcel(parcel: Parcel, flags: Int) {
@@ -41,7 +21,7 @@ class CreditCard() : Parcelable {
} }
fun getExpirationMonth(): String { fun getExpirationMonth(): String {
return if (expiration.length == 4) { return if (expiration?.length == 4) {
expiration.substring(0, 2) expiration.substring(0, 2)
} else { } else {
"" ""
@@ -49,39 +29,17 @@ class CreditCard() : Parcelable {
} }
fun getExpirationYear(): String { fun getExpirationYear(): String {
return if (expiration.length == 4) { return if (expiration?.length == 4) {
expiration.substring(2, 4) expiration.substring(2, 4)
} else { } 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 { override fun describeContents(): Int {
return 0 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> { companion object CREATOR : Parcelable.Creator<CreditCard> {
override fun createFromParcel(parcel: Parcel): CreditCard { override fun createFromParcel(parcel: Parcel): CreditCard {
return CreditCard(parcel) return CreditCard(parcel)

View File

@@ -39,9 +39,9 @@ class EntryInfo : NodeInfo {
var attachments: List<Attachment> = ArrayList() var attachments: List<Attachment> = ArrayList()
var otpModel: OtpModel? = null var otpModel: OtpModel? = null
constructor(): super() constructor() : super()
constructor(parcel: Parcel): super(parcel) { constructor(parcel: Parcel) : super(parcel) {
id = parcel.readString() ?: id id = parcel.readString() ?: id
username = parcel.readString() ?: username username = parcel.readString() ?: username
password = parcel.readString() ?: password password = parcel.readString() ?: password
@@ -133,8 +133,7 @@ class EntryInfo : NodeInfo {
val webDomainToStore = "$webScheme://$webDomain" val webDomainToStore = "$webScheme://$webDomain"
if (database?.allowEntryCustomFields() != true || url.isEmpty()) { if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
url = webDomainToStore url = webDomainToStore
} } else if (url != webDomainToStore) {
else if (url != webDomainToStore){
// Save web domain in custom field // Save web domain in custom field
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME, addUniqueField(Field(WEB_DOMAIN_FIELD_NAME,
ProtectedString(false, webDomainToStore)), ProtectedString(false, webDomainToStore)),
@@ -153,6 +152,38 @@ class EntryInfo : NodeInfo {
} }
} }
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
registerInfo.username?.let {
username = it
}
registerInfo.password?.let {
password = it
}
if (database?.allowEntryCustomFields() == true) {
val creditCard: CreditCard? = registerInfo.cc
creditCard?.let { cc ->
cc.cardholder?.let {
val v = ProtectedString(false, it)
addUniqueField(Field(CreditCardCustomFields.CC_CARDHOLDER_FIELD_NAME, v))
}
cc.expiration?.let {
val v = ProtectedString(false, it)
addUniqueField(Field(CreditCardCustomFields.CC_EXP_FIELD_NAME, v))
}
cc.number?.let {
val v = ProtectedString(false, it)
addUniqueField(Field(CreditCardCustomFields.CC_NUMBER_FIELD_NAME, v))
}
cc.cvv?.let {
val v = ProtectedString(true, it)
addUniqueField(Field(CreditCardCustomFields.CC_CVV_FIELD_NAME, v))
}
}
}
}
companion object { companion object {
const val WEB_DOMAIN_FIELD_NAME = "URL" const val WEB_DOMAIN_FIELD_NAME = "URL"

View File

@@ -5,18 +5,21 @@ import android.os.Parcelable
data class RegisterInfo(val searchInfo: SearchInfo, data class RegisterInfo(val searchInfo: SearchInfo,
val username: String?, val username: String?,
val password: String?): Parcelable { val password: String?,
val cc: CreditCard?): Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readParcelable(SearchInfo::class.java.classLoader) ?: SearchInfo(), parcel.readParcelable(SearchInfo::class.java.classLoader) ?: SearchInfo(),
parcel.readString() ?: "", parcel.readString() ?: "",
parcel.readString() ?: "") { parcel.readString() ?: "",
parcel.readParcelable(CreditCard::class.java.classLoader)) {
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(searchInfo, flags) parcel.writeParcelable(searchInfo, flags)
parcel.writeString(username) parcel.writeString(username)
parcel.writeString(password) parcel.writeString(password)
parcel.writeParcelable(cc, flags)
} }
override fun describeContents(): Int { override fun describeContents(): Int {

View File

@@ -357,7 +357,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
activateCopyButton(allowCopy) activateCopyButton(allowCopy)
assignCopyButtonClickListener(onCopyButtonClickListener) assignCopyButtonClickListener(onCopyButtonClickListener)
applyFontVisibility(fontInVisibility) applyFontVisibility(fontInVisibility)
checkCreditCardDetails(fieldName)
} }
entryCustomField?.let { entryCustomField?.let {

View File

@@ -108,23 +108,6 @@ class EntryField @JvmOverloads constructor(context: Context,
} }
} }
private fun setValueTextColor(color: Int) {
valueView.setTextColor(color)
}
fun checkCreditCardDetails(fieldName: String) {
val value = valueView.text
when (fieldName) {
CreditCardCustomFields.CC_CVV_FIELD_NAME ->
if (value.length < 3 || value.length > 4) setValueTextColor(Color.RED)
CreditCardCustomFields.CC_EXP_FIELD_NAME ->
if (value.length != 4) setValueTextColor(Color.RED)
CreditCardCustomFields.CC_NUMBER_FIELD_NAME ->
if (value.length != 16) setValueTextColor(Color.RED)
}
}
fun setAutoLink() { fun setAutoLink() {
if (!isProtected) linkify() if (!isProtected) linkify()
changeProtectedValueParameters() changeProtectedValueParameters()

View File

@@ -48,8 +48,6 @@
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:labelFor="@+id/creditCardNumberField" android:labelFor="@+id/creditCardNumberField"
android:text="@string/cc_cardholder" android:text="@string/cc_cardholder"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -62,8 +60,6 @@
android:ems="10" android:ems="10"
android:focusedByDefault="true" android:focusedByDefault="true"
android:hint="@string/cc_cardholder" android:hint="@string/cc_cardholder"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="textPersonName" android:inputType="textPersonName"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardholderNameLabel" /> app:layout_constraintTop_toBottomOf="@+id/creditCardholderNameLabel" />
@@ -73,8 +69,6 @@
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:labelFor="@+id/creditCardNumberField" android:labelFor="@+id/creditCardNumberField"
android:text="@string/cc_number" android:text="@string/cc_number"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -87,8 +81,6 @@
android:ems="10" android:ems="10"
android:focusedByDefault="true" android:focusedByDefault="true"
android:hint="@string/cc_number" android:hint="@string/cc_number"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="number" android:inputType="number"
android:maxLength="16" android:maxLength="16"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -99,8 +91,6 @@
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:text="@string/cc_expiration" android:text="@string/cc_expiration"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/creditCardNumberField" /> app:layout_constraintTop_toBottomOf="@+id/creditCardNumberField" />
@@ -129,7 +119,6 @@
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAutofill="no"
android:labelFor="@+id/creditCardSecurityCode" android:labelFor="@+id/creditCardSecurityCode"
android:text="@string/cc_security_code" android:text="@string/cc_security_code"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@@ -530,32 +530,7 @@
<string name="unit_kibibyte">KiB</string> <string name="unit_kibibyte">KiB</string>
<string name="unit_mebibyte">MiB</string> <string name="unit_mebibyte">MiB</string>
<string name="unit_gibibyte">GiB</string> <string name="unit_gibibyte">GiB</string>
<string-array name="month_array">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
</string-array>
<string-array name="year_array">
<item>20</item>
<item>21</item>
<item>22</item>
<item>23</item>
<item>24</item>
<item>25</item>
<item>26</item>
<item>27</item>
<item>28</item>
<item>29</item>
</string-array>
<string-array name="timeout_options"> <string-array name="timeout_options">
<item>5 seconds</item> <item>5 seconds</item>
<item>10 seconds</item> <item>10 seconds</item>