Merge branch 'master' of git://github.com/uduerholz/KeePassDX into uduerholz-master

This commit is contained in:
J-Jamet
2021-05-03 15:48:04 +02:00
21 changed files with 1023 additions and 87 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}
}
}
}
}
}

View File

@@ -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 = {

View File

@@ -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()
}
/**

View File

@@ -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,

View File

@@ -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 {

View 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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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 ->

View File

@@ -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,

View File

@@ -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")

View 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>

View File

@@ -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"

View 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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>