mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'master' of git://github.com/uduerholz/KeePassDX into uduerholz-master
This commit is contained in:
@@ -49,6 +49,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
|
||||
@@ -322,6 +323,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
|
||||
@@ -332,7 +334,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 {
|
||||
@@ -346,6 +348,7 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
@@ -439,15 +442,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,
|
||||
@@ -189,16 +191,15 @@ class EntryEditActivity : LockingActivity(),
|
||||
val registerInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
registerInfo?.username?.let {
|
||||
tempEntryInfo?.username = it
|
||||
}
|
||||
registerInfo?.password?.let {
|
||||
tempEntryInfo?.password = it
|
||||
}
|
||||
|
||||
searchInfo?.let { tempSearchInfo ->
|
||||
tempEntryInfo?.saveSearchInfo(mDatabase, tempSearchInfo)
|
||||
}
|
||||
|
||||
registerInfo?.let { regInfo ->
|
||||
tempEntryInfo?.saveRegisterInfo(mDatabase, regInfo)
|
||||
}
|
||||
|
||||
// Build fragment to manage entry modification
|
||||
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
||||
if (entryEditFragment == null) {
|
||||
@@ -405,6 +406,29 @@ class EntryEditActivity : LockingActivity(),
|
||||
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
|
||||
}
|
||||
|
||||
private fun addNewCreditCard() {
|
||||
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")
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new customized field
|
||||
*/
|
||||
@@ -455,6 +479,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 +639,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 +718,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
addNewCustomField()
|
||||
return true
|
||||
}
|
||||
R.id.menu_add_cc -> {
|
||||
addNewCreditCard()
|
||||
return true
|
||||
}
|
||||
R.id.menu_add_attachment -> {
|
||||
addNewAttachment()
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
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.CreditCard
|
||||
import com.kunzisoft.keepass.model.CreditCardCustomFields.buildAllFields
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import java.util.*
|
||||
|
||||
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 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 {
|
||||
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()?.substring(2,4) ?: ""
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
mCreditCard?.cardholder?.let {
|
||||
mCcCardholderName?.setText(it)
|
||||
}
|
||||
mCreditCard?.number?.let {
|
||||
mCcCardNumber?.setText(it)
|
||||
}
|
||||
mCreditCard?.cvv?.let {
|
||||
mCcSecurityCode?.setText(it)
|
||||
}
|
||||
}
|
||||
|
||||
val months = arrayOf("01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12")
|
||||
mCcExpirationMonthSpinner?.let { spinner ->
|
||||
spinner.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, months)
|
||||
mCreditCard?.let { cc ->
|
||||
spinner.setSelection(getIndex(spinner, cc.getExpirationMonth()))
|
||||
}
|
||||
}
|
||||
|
||||
val years = arrayOfNulls<String>(5)
|
||||
val year = Calendar.getInstance()[Calendar.YEAR]
|
||||
for (i in years.indices) {
|
||||
years[i] = (year + i).toString()
|
||||
}
|
||||
mCcExpirationYearSpinner?.let { spinner ->
|
||||
spinner.adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item, years)
|
||||
mCreditCard?.let { cc ->
|
||||
spinner.setSelection(getIndex(spinner, "20" + cc.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() == value) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
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,88 @@ 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 format MM)
|
||||
val monthString = field.protectedValue.stringValue.substring(0, 2)
|
||||
val month = monthString.toIntOrNull() ?: 0
|
||||
if (month < 1 || month > 12) continue
|
||||
|
||||
// get year (year in database entry is stored as String in the format YY)
|
||||
val yearString = field.protectedValue.stringValue.substring(2, 4)
|
||||
val year = "20$yearString".toIntOrNull() ?: 0
|
||||
if (year == 0) 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()
|
||||
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(monthString))
|
||||
} else {
|
||||
if (struct.ccExpMonthOptions != null) {
|
||||
// index starts at 0
|
||||
builder.setValue(it, AutofillValue.forList(month - 1))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
}
|
||||
}
|
||||
}
|
||||
struct.ccExpDateYearId?.let {
|
||||
var autofillValue: AutofillValue? = null
|
||||
|
||||
struct.ccExpYearOptions?.let { options ->
|
||||
var yearIndex = options.indexOf(yearString)
|
||||
|
||||
if (yearIndex == -1) {
|
||||
yearIndex = options.indexOf("20$yearString")
|
||||
}
|
||||
if (yearIndex != -1) {
|
||||
autofillValue = AutofillValue.forList(yearIndex)
|
||||
builder.setValue(it, autofillValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (autofillValue == null) {
|
||||
builder.setValue(it, AutofillValue.forText("20$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 +209,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 +239,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 +274,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 +291,7 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add inline suggestion for new IME and dataset
|
||||
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||
@@ -217,9 +301,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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.model.CreditCard
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
@@ -101,6 +102,15 @@ class KeeAutofillService : AutofillService() {
|
||||
callback)
|
||||
}
|
||||
}
|
||||
// TODO does it make sense to disable autofill here? how long?
|
||||
// else {
|
||||
// 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())
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,23 +165,40 @@ class KeeAutofillService : AutofillService() {
|
||||
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) {
|
||||
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
|
||||
parseResult.passwordId?.let { passwordInfo ->
|
||||
parseResult.usernameId?.let { usernameInfo ->
|
||||
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||
info.add(usernameInfo)
|
||||
requiredIds.add(usernameInfo)
|
||||
}
|
||||
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||
info.add(passwordInfo)
|
||||
requiredIds.add(passwordInfo)
|
||||
}
|
||||
if (info.isNotEmpty()) {
|
||||
responseBuilder.setSaveInfo(
|
||||
SaveInfo.Builder(types, info.toTypedArray()).build()
|
||||
)
|
||||
// or a credit card form
|
||||
if (requiredIds.isEmpty()) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,14 +250,27 @@ class KeeAutofillService : AutofillService() {
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
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
|
||||
val registerInfo = RegisterInfo(SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
val registerInfo = RegisterInfo(
|
||||
SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
parseResult.usernameValue?.textValue?.toString(),
|
||||
parseResult.passwordValue?.textValue?.toString())
|
||||
parseResult.passwordValue?.textValue?.toString(),
|
||||
creditCard)
|
||||
|
||||
// TODO Callback in each activity #765
|
||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||
|
||||
@@ -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,29 @@ 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 if (result?.passwordId != null || result?.ccnId != null)
|
||||
result
|
||||
else
|
||||
null
|
||||
} 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 +99,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 +131,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 +140,96 @@ 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" -> {
|
||||
Log.d(TAG, "AUTOFILL cc-name hint")
|
||||
result?.ccNameId = autofillId
|
||||
result?.ccName = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
it == View.AUTOFILL_HINT_CREDIT_CARD_NUMBER || it == "cc-number" -> {
|
||||
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_NUMBER hint")
|
||||
result?.ccnId = autofillId
|
||||
result?.ccNumber = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
// 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
|
||||
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")
|
||||
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" -> {
|
||||
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR hint")
|
||||
result?.ccExpDateYearId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
result?.ccExpYearOptions = node.autofillOptions
|
||||
}
|
||||
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" -> {
|
||||
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH hint")
|
||||
result?.ccExpDateMonthId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
result?.ccExpMonthOptions = node.autofillOptions
|
||||
}
|
||||
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" -> {
|
||||
Log.d(TAG, "AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE hint")
|
||||
result?.cvvId = autofillId
|
||||
result?.cvv = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
// 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 +254,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 +302,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)}")
|
||||
}
|
||||
@@ -243,7 +326,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,
|
||||
@@ -265,16 +347,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 -> {
|
||||
@@ -288,6 +369,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
|
||||
@@ -302,6 +384,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)
|
||||
@@ -314,6 +401,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 {
|
||||
@@ -322,6 +445,15 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
passwordId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
ccNameId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
ccnId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
cvvId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
return all.toTypedArray()
|
||||
}
|
||||
|
||||
@@ -339,6 +471,46 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
if (allowSaveValues && field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var ccName: String? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
var ccNumber: String? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// format MMYY
|
||||
var ccExpirationValue: String? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// for year of CC expiration date: YY
|
||||
var ccExpDateYearValue = 0
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// for month of CC expiration date: MM
|
||||
var ccExpDateMonthValue = 0
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// the security code for the credit card (also called CVV)
|
||||
var cvv: String? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
52
app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt
Normal file
52
app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
data class CreditCard(val cardholder: String?, val number: String?,
|
||||
val expiration: String?, val cvv: String?) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readString()) {
|
||||
}
|
||||
|
||||
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 describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,9 @@ class EntryInfo : NodeInfo {
|
||||
var attachments: List<Attachment> = ArrayList()
|
||||
var otpModel: OtpModel? = null
|
||||
|
||||
constructor(): super()
|
||||
constructor() : super()
|
||||
|
||||
constructor(parcel: Parcel): super(parcel) {
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
id = parcel.readString() ?: id
|
||||
username = parcel.readString() ?: username
|
||||
password = parcel.readString() ?: password
|
||||
@@ -133,8 +133,7 @@ class EntryInfo : NodeInfo {
|
||||
val webDomainToStore = "$webScheme://$webDomain"
|
||||
if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
|
||||
url = webDomainToStore
|
||||
}
|
||||
else if (url != webDomainToStore){
|
||||
} else if (url != webDomainToStore) {
|
||||
// Save web domain in custom field
|
||||
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME,
|
||||
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 {
|
||||
|
||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||
|
||||
@@ -5,18 +5,21 @@ import android.os.Parcelable
|
||||
|
||||
data class RegisterInfo(val searchInfo: SearchInfo,
|
||||
val username: String?,
|
||||
val password: String?): Parcelable {
|
||||
val password: String?,
|
||||
val cc: CreditCard?): Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readParcelable(SearchInfo::class.java.classLoader) ?: SearchInfo(),
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "") {
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelable(CreditCard::class.java.classLoader)) {
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(searchInfo, flags)
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeParcelable(cc, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.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,47 @@ 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)
|
||||
}
|
||||
|
||||
entryCustomField?.let {
|
||||
creditCardFieldsListView.addView(it)
|
||||
}
|
||||
|
||||
showOrHideCreditCardContainer(false)
|
||||
}
|
||||
|
||||
fun clearExtraFields() {
|
||||
extraFieldsListView.removeAllViews()
|
||||
showOrHideExtraFieldsContainer(true)
|
||||
}
|
||||
|
||||
fun clearCreditCardFields() {
|
||||
creditCardFieldsListView.removeAllViews()
|
||||
showOrHideCreditCardContainer(true)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
@@ -332,7 +391,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 +408,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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
143
app/src/main/res/layout/entry_cc_details_dialog.xml
Normal file
143
app/src/main/res/layout/entry_cc_details_dialog.xml
Normal file
@@ -0,0 +1,143 @@
|
||||
<?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: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: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: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: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: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: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>
|
||||
|
||||
@@ -363,12 +363,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>
|
||||
@@ -536,6 +541,31 @@
|
||||
<string name="unit_kibibyte">KiB</string>
|
||||
<string name="unit_mebibyte">MiB</string>
|
||||
<string name="unit_gibibyte">GiB</string>
|
||||
<string-array name="timeout_options">
|
||||
<item>5 seconds</item>
|
||||
<item>10 seconds</item>
|
||||
<item>20 seconds</item>
|
||||
<item>30 seconds</item>
|
||||
<item>1 minute</item>
|
||||
<item>5 minutes</item>
|
||||
<item>15 minutes</item>
|
||||
<item>30 minutes</item>
|
||||
<item>Never</item>
|
||||
</string-array>
|
||||
<string-array name="large_timeout_options">
|
||||
<item>5 minutes</item>
|
||||
<item>15 minutes</item>
|
||||
<item>30 minutes</item>
|
||||
<item>1 hour</item>
|
||||
<item>2 hours</item>
|
||||
<item>5 hours</item>
|
||||
<item>10 hours</item>
|
||||
<item>24 hours</item>
|
||||
<item>48 hours</item>
|
||||
<item>1 week</item>
|
||||
<item>1 month</item>
|
||||
<item>Never</item>
|
||||
</string-array>
|
||||
<string-array name="list_size_options">
|
||||
<item>Small</item>
|
||||
<item>Medium</item>
|
||||
|
||||
Reference in New Issue
Block a user