Merge branch 'feature/Fragment_Edit_Entry' into develop #686

This commit is contained in:
J-Jamet
2020-09-16 11:36:32 +02:00
19 changed files with 769 additions and 881 deletions

View File

@@ -309,9 +309,11 @@ class EntryActivity : LockingActivity() {
entryContentsView?.assignNotes(entry.notes) entryContentsView?.assignNotes(entry.notes)
// Assign custom fields // Assign custom fields
if (entry.allowCustomFields()) { if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields() entryContentsView?.clearExtraFields()
for ((label, value) in entry.customFields) { entry.getExtraFields().forEach { field ->
val label = field.name
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) { if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) { entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {

View File

@@ -35,6 +35,7 @@ import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -45,11 +46,13 @@ import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
@@ -60,7 +63,6 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.updateLockPaddingLeft import com.kunzisoft.keepass.view.updateLockPaddingLeft
@@ -83,20 +85,16 @@ class EntryEditActivity : LockingActivity(),
// Refs of an entry and group in database, are not modifiable // Refs of an entry and group in database, are not modifiable
private var mEntry: Entry? = null private var mEntry: Entry? = null
private var mParent: Group? = null private var mParent: Group? = null
// New or copy of mEntry in the database to be modifiable
private var mNewEntry: Entry? = null
private var mIsNew: Boolean = false private var mIsNew: Boolean = false
// Views // Views
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null private var scrollView: NestedScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null private var entryEditFragment: EntryEditFragment? = null
private var entryEditAddToolBar: Toolbar? = null private var entryEditAddToolBar: Toolbar? = null
private var validateButton: View? = null private var validateButton: View? = null
private var lockView: View? = null private var lockView: View? = null
private var mFocusedEditExtraField: FocusedEditField? = null
// To manage attachments // To manage attachments
private var mSelectFileHelper: SelectFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
@@ -121,22 +119,6 @@ class EntryEditActivity : LockingActivity(),
scrollView = findViewById(R.id.entry_edit_scroll) scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
entryEditContentsView = findViewById(R.id.entry_edit_contents)
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryEditContentsView?.onDateClickListener = View.OnClickListener {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
}
}
entryEditContentsView?.entryPasswordGeneratorView?.setOnClickListener {
openPasswordGenerator()
}
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener { lockView?.setOnClickListener {
lockAndExit() lockAndExit()
@@ -151,6 +133,8 @@ class EntryEditActivity : LockingActivity(),
// Likely the app has been killed exit the activity // Likely the app has been killed exit the activity
mDatabase = Database.getInstance() mDatabase = Database.getInstance()
var tempEntryInfo: EntryInfo? = null
// Entry is retrieve, it's an entry to update // Entry is retrieve, it's an entry to update
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
mIsNew = false mIsNew = false
@@ -166,74 +150,77 @@ class EntryEditActivity : LockingActivity(),
entry.parent = mParent entry.parent = mParent
} }
} }
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
// Create the new entry from the current one
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
mEntry?.let { entry ->
// Create a copy to modify
mNewEntry = Entry(entry).also { newEntry ->
// WARNING Remove the parent to keep memory with parcelable
newEntry.removeParent()
}
}
}
} }
// Parent is retrieve, it's a new entry to create // Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
mIsNew = true mIsNew = true
// Create an empty new entry
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
mNewEntry = mDatabase?.createEntry()
}
mParent = mDatabase?.getGroupById(it) mParent = mDatabase?.getGroupById(it)
// Add the default icon from parent if not a folder // Add the default icon from parent if not a folder
val parentIcon = mParent?.icon val parentIcon = mParent?.icon
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
// Set default icon
if (parentIcon != null if (parentIcon != null
&& parentIcon.iconId != IconImage.UNKNOWN_ID && parentIcon.iconId != IconImage.UNKNOWN_ID
&& parentIcon.iconId != IconImageStandard.FOLDER) { && parentIcon.iconId != IconImageStandard.FOLDER) {
temporarilySaveAndShowSelectedIcon(parentIcon) tempEntryInfo?.icon = parentIcon
} else { }
mDatabase?.drawFactory?.let { iconFactory -> // Set default username
entryEditContentsView?.setDefaultIcon(iconFactory) tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
}
// Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) {
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
}
supportFragmentManager.beginTransaction()
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
.commit()
entryEditFragment?.apply {
drawFactory = mDatabase?.drawFactory
setOnDateClickListener = View.OnClickListener {
expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
} }
} }
setOnPasswordGeneratorClickListener = View.OnClickListener {
openPasswordGenerator()
}
// Add listener to the icon
setOnIconViewClickListener = View.OnClickListener {
IconPickerDialogFragment.launch(this@EntryEditActivity)
}
setOnRemoveAttachment = { attachment ->
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
}
setOnEditCustomField = { field ->
editCustomField(field)
}
} }
// Retrieve the new entry after an orientation change // Retrieve temp attachments in case of deletion
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) == true) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
}
if (savedInstanceState?.containsKey(EXTRA_FIELD_FOCUSED_ENTRY) == true) {
mFocusedEditExtraField = savedInstanceState.getParcelable(EXTRA_FIELD_FOCUSED_ENTRY)
}
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) { if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
} }
// Close the activity if entry or parent can't be retrieve
if (mNewEntry == null || mParent == null) {
finish()
return
}
populateViewsWithEntry(mNewEntry!!)
// Assign title // Assign title
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry) title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
// Add listener to the icon
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Bottom Bar // Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar) entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply { entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu) menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply { menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mNewEntry?.allowCustomFields() == true val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField isEnabled = allowCustomField
isVisible = allowCustomField isVisible = allowCustomField
} }
@@ -285,8 +272,22 @@ class EntryEditActivity : LockingActivity(),
when (actionTask) { when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK, ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> { ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess) try {
finish() if (result.isSuccess) {
var newNodes: List<Node> = ArrayList()
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
mDatabase?.let { database ->
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
}
}
if (newNodes.size == 1) {
mEntry = newNodes[0] as Entry?
finish()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry after database action", e)
}
} }
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionError(result)
@@ -312,13 +313,12 @@ class EntryEditActivity : LockingActivity(),
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) { override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
when (entryAttachmentState.downloadState) { when (entryAttachmentState.downloadState) {
AttachmentState.START -> { AttachmentState.START -> {
entryEditContentsView?.apply { entryEditFragment?.apply {
// When only one attachment is allowed // When only one attachment is allowed
if (!mAllowMultipleAttachments) { if (!mAllowMultipleAttachments) {
clearAttachments() clearAttachments()
} }
putAttachment(entryAttachmentState) putAttachment(entryAttachmentState)
requestLayout()
// Scroll to the attachment position // Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) { getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt()) scrollView?.smoothScrollTo(0, it.toInt())
@@ -326,10 +326,10 @@ class EntryEditActivity : LockingActivity(),
} }
} }
AttachmentState.IN_PROGRESS -> { AttachmentState.IN_PROGRESS -> {
entryEditContentsView?.putAttachment(entryAttachmentState) entryEditFragment?.putAttachment(entryAttachmentState)
} }
AttachmentState.COMPLETE -> { AttachmentState.COMPLETE -> {
entryEditContentsView?.apply { entryEditFragment?.apply {
putAttachment(entryAttachmentState) putAttachment(entryAttachmentState)
// Scroll to the attachment position // Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) { getAttachmentViewPosition(entryAttachmentState) {
@@ -338,7 +338,7 @@ class EntryEditActivity : LockingActivity(),
} }
} }
AttachmentState.ERROR -> { AttachmentState.ERROR -> {
entryEditContentsView?.removeAttachment(entryAttachmentState) entryEditFragment?.removeAttachment(entryAttachmentState)
coordinatorLayout?.let { coordinatorLayout?.let {
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
} }
@@ -356,81 +356,6 @@ class EntryEditActivity : LockingActivity(),
super.onPause() super.onPause()
} }
private fun populateViewsWithEntry(newEntry: Entry) {
// Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(newEntry)
// Set info in temp parameters
temporarilySaveAndShowSelectedIcon(newEntry.icon)
// Set info in view
entryEditContentsView?.apply {
title = newEntry.title
username = if (mIsNew && newEntry.username.isEmpty())
mDatabase?.defaultUsername ?: ""
else
newEntry.username
url = newEntry.url
password = newEntry.password
expires = newEntry.expires
if (expires)
expiresDate = newEntry.expiryTime
notes = newEntry.notes
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
Field(it.key, it.value)
}, {
editCustomField(it)
}, mFocusedEditExtraField)
mDatabase?.binaryPool?.let { binaryPool ->
assignAttachments(newEntry.getAttachments(binaryPool).toSet(), StreamDirection.UPLOAD) { attachment ->
// Remove entry by clicking trash button
newEntry.removeAttachment(attachment)
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
}
}
}
}
private fun populateEntryWithViews(newEntry: Entry) {
mDatabase?.startManageEntry(newEntry)
newEntry.apply {
// Build info from view
entryEditContentsView?.let { entryView ->
removeAllFields()
title = entryView.title
username = entryView.username
url = entryView.url
password = entryView.password
expires = entryView.expires
if (entryView.expires) {
expiryTime = entryView.expiresDate
}
notes = entryView.notes
entryView.getExtraFields().forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
}
mDatabase?.binaryPool?.let { binaryPool ->
entryView.getAttachments().forEach {
putAttachment(it, binaryPool)
}
}
mFocusedEditExtraField = entryView.getExtraFieldFocused()
}
}
mDatabase?.stopManageEntry(newEntry)
}
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
mNewEntry?.icon = icon
mDatabase?.drawFactory?.let { iconDrawFactory ->
entryEditContentsView?.setIcon(iconDrawFactory, icon)
}
}
/** /**
* Open the password generator fragment * Open the password generator fragment
*/ */
@@ -450,20 +375,17 @@ class EntryEditActivity : LockingActivity(),
} }
override fun onNewCustomFieldApproved(newField: Field) { override fun onNewCustomFieldApproved(newField: Field) {
entryEditContentsView?.apply { entryEditFragment?.apply {
putExtraField(newField) putExtraField(newField)
getExtraFieldViewPosition(newField) { position ->
scrollView?.smoothScrollTo(0, position.toInt())
}
} }
} }
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) { override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
entryEditContentsView?.replaceExtraField(oldField, newField) entryEditFragment?.replaceExtraField(oldField, newField)
} }
override fun onDeleteCustomFieldApproved(oldField: Field) { override fun onDeleteCustomFieldApproved(oldField: Field) {
entryEditContentsView?.removeExtraField(oldField) entryEditFragment?.removeExtraField(oldField)
} }
/** /**
@@ -497,8 +419,8 @@ class EntryEditActivity : LockingActivity(),
mDatabase?.buildNewBinary(applicationContext.filesDir, false, compression)?.let { binaryAttachment -> mDatabase?.buildNewBinary(applicationContext.filesDir, false, compression)?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment) val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment // Ask to replace the current attachment
if ((mDatabase?.allowMultipleAttachments != true && entryEditContentsView?.containsAttachment() == true) || if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
entryEditContentsView?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) { entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment) ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
.show(supportFragmentManager, "replacementFileFragment") .show(supportFragmentManager, "replacementFileFragment")
} else { } else {
@@ -533,7 +455,7 @@ class EntryEditActivity : LockingActivity(),
private fun setupOTP() { private fun setupOTP() {
// Retrieve the current otpElement if exists // Retrieve the current otpElement if exists
// and open the dialog to set up the OTP // and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel) SetOTPDialogFragment.build(entryEditFragment?.getEntryInfo()?.otpModel)
.show(supportFragmentManager, "addOTPDialog") .show(supportFragmentManager, "addOTPDialog")
} }
@@ -541,19 +463,22 @@ class EntryEditActivity : LockingActivity(),
* Saves the new entry or update an existing entry in the database * Saves the new entry or update an existing entry in the database
*/ */
private fun saveEntry() { private fun saveEntry() {
// Launch a validation and show the error if present // Get the temp entry
if (entryEditContentsView?.isValid() == true) { entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
// Clone the entry
mNewEntry?.let { newEntry ->
// WARNING Add the parent previously deleted if (mIsNew) {
newEntry.parent = mEntry?.parent // Create new one
mDatabase?.createEntry()
} else {
// Create a clone
Entry(mEntry!!)
}?.let { newEntry ->
newEntry.setEntryInfo(mDatabase, newEntryInfo)
// Build info // Build info
newEntry.lastAccessTime = DateInstant() newEntry.lastAccessTime = DateInstant()
newEntry.lastModificationTime = DateInstant() newEntry.lastModificationTime = DateInstant()
populateEntryWithViews(newEntry)
// Delete temp attachment if not used // Delete temp attachment if not used
mTempAttachments.forEach { mTempAttachments.forEach {
mDatabase?.binaryPool?.let { binaryPool -> mDatabase?.binaryPool?.let { binaryPool ->
@@ -594,30 +519,23 @@ class EntryEditActivity : LockingActivity(),
menu.findItem(R.id.menu_save_database)?.isVisible = false menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu) MenuUtil.contributionMenuInflater(inflater, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
return true return true
} }
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordGeneratorView: View? = entryEditContentsView?.entryPasswordGeneratorView override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
val generatePasswordEducationPerformed = passwordGeneratorView != null entryEditActivityEducation?.let {
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( Handler().post { performedNextEducation(it) }
passwordGeneratorView, }
{ return super.onPrepareOptionsMenu(menu)
openPasswordGenerator() }
},
{ fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
performedNextEducation(entryEditActivityEducation) if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
}
)
if (!generatePasswordEducationPerformed) {
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field) val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
val addNewFieldEducationPerformed = mNewEntry != null val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
&& mNewEntry!!.allowCustomFields() && addNewFieldView != null && addNewFieldView != null
&& addNewFieldView.visibility == View.VISIBLE && addNewFieldView.isVisible
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView, addNewFieldView,
{ {
@@ -629,7 +547,8 @@ class EntryEditActivity : LockingActivity(),
) )
if (!addNewFieldEducationPerformed) { if (!addNewFieldEducationPerformed) {
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment) val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
val addAttachmentEducationPerformed = attachmentView != null && attachmentView.visibility == View.VISIBLE val addAttachmentEducationPerformed = attachmentView != null
&& attachmentView.isVisible
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation( && entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView, attachmentView,
{ {
@@ -641,7 +560,8 @@ class EntryEditActivity : LockingActivity(),
) )
if (!addAttachmentEducationPerformed) { if (!addAttachmentEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp) val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null && setupOtpView.visibility == View.VISIBLE setupOtpView != null
&& setupOtpView.isVisible
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation( && entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView, setupOtpView,
{ {
@@ -674,18 +594,15 @@ class EntryEditActivity : LockingActivity(),
// Update the otp field with otpauth:// url // Update the otp field with otpauth:// url
val otpField = OtpEntryFields.buildOtpField(otpElement, val otpField = OtpEntryFields.buildOtpField(otpElement,
mEntry?.title, mEntry?.username) mEntry?.title, mEntry?.username)
mEntry?.putExtraField(otpField.name, otpField.protectedValue) mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue))
entryEditContentsView?.apply { entryEditFragment?.apply {
putExtraField(otpField) putExtraField(otpField)
getExtraFieldViewPosition(otpField) { position ->
scrollView?.smoothScrollTo(0, position.toInt())
}
} }
} }
override fun iconPicked(bundle: Bundle) { override fun iconPicked(bundle: Bundle) {
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon -> IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
temporarilySaveAndShowSelectedIcon(icon) entryEditFragment?.icon = icon
} }
} }
@@ -693,9 +610,9 @@ class EntryEditActivity : LockingActivity(),
// To fix android 4.4 issue // To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice // https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
if (datePicker?.isShown == true) { if (datePicker?.isShown == true) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate -> entryEditFragment?.expiryTime?.date?.let { expiresDate ->
// Save the date // Save the date
entryEditContentsView?.expiresDate = entryEditFragment?.expiryTime =
DateInstant(DateTime(expiresDate) DateInstant(DateTime(expiresDate)
.withYear(year) .withYear(year)
.withMonthOfYear(month + 1) .withMonthOfYear(month + 1)
@@ -712,9 +629,9 @@ class EntryEditActivity : LockingActivity(),
} }
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) { override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate -> entryEditFragment?.expiryTime?.date?.let { expiresDate ->
// Save the date // Save the date
entryEditContentsView?.expiresDate = entryEditFragment?.expiryTime =
DateInstant(DateTime(expiresDate) DateInstant(DateTime(expiresDate)
.withHourOfDay(hours) .withHourOfDay(hours)
.withMinuteOfHour(minutes) .withMinuteOfHour(minutes)
@@ -723,14 +640,6 @@ class EntryEditActivity : LockingActivity(),
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_ENTRY, it)
}
mFocusedEditExtraField?.let {
outState.putParcelable(EXTRA_FIELD_FOCUSED_ENTRY, it)
}
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments) outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
@@ -739,7 +648,7 @@ class EntryEditActivity : LockingActivity(),
override fun acceptPassword(bundle: Bundle) { override fun acceptPassword(bundle: Bundle) {
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let { bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
entryEditContentsView?.password = it entryEditFragment?.password = it
} }
entryEditActivityEducation?.let { entryEditActivityEducation?.let {
@@ -763,10 +672,10 @@ class EntryEditActivity : LockingActivity(),
override fun finish() { override fun finish() {
// Assign entry callback as a result in all case // Assign entry callback as a result in all case
try { try {
mNewEntry?.let { mEntry?.let { entry ->
val bundle = Bundle() val bundle = Bundle()
val intentEntry = Intent() val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry) bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry)
intentEntry.putExtras(bundle) intentEntry.putExtras(bundle)
if (mIsNew) { if (mIsNew) {
setResult(ADD_ENTRY_RESULT_CODE, intentEntry) setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
@@ -790,8 +699,6 @@ class EntryEditActivity : LockingActivity(),
const val KEY_PARENT = "parent" const val KEY_PARENT = "parent"
// SaveInstanceState // SaveInstanceState
const val KEY_NEW_ENTRY = "new_entry"
const val EXTRA_FIELD_FOCUSED_ENTRY = "EXTRA_FIELD_FOCUSED_ENTRY"
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS" const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
// Keys for callback // Keys for callback
@@ -800,6 +707,8 @@ class EntryEditActivity : LockingActivity(),
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129 const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY" const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
/** /**
* Launch EntryEditActivity to update an existing entry * Launch EntryEditActivity to update an existing entry
* *

View File

@@ -0,0 +1,535 @@
/*
* 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/>.
*
*/
package com.kunzisoft.keepass.activities
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
class EntryEditFragment: StylishFragment() {
private lateinit var entryTitleLayoutView: TextInputLayout
private lateinit var entryTitleView: EditText
private lateinit var entryIconView: ImageView
private lateinit var entryUserNameView: EditText
private lateinit var entryUrlView: EditText
private lateinit var entryPasswordLayoutView: TextInputLayout
private lateinit var entryPasswordView: EditText
private lateinit var entryPasswordGeneratorView: View
private lateinit var entryExpiresCheckBox: CompoundButton
private lateinit var entryExpiresTextView: TextView
private lateinit var entryNotesView: EditText
private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup
private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
private var fontInVisibility: Boolean = false
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
var drawFactory: IconDrawableFactory? = null
var setOnDateClickListener: View.OnClickListener? = null
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
var setOnIconViewClickListener: View.OnClickListener? = null
var setOnEditCustomField: ((Field) -> Unit)? = null
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
// Elements to modify the current entry
private var mEntryInfo = EntryInfo()
private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit_contents, container, false)
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
entryIconView.setOnClickListener {
setOnIconViewClickListener?.onClick(it)
}
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
entryPasswordGeneratorView.setOnClickListener {
setOnPasswordGeneratorClickListener?.onClick(it)
}
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
entryExpiresTextView.setOnClickListener {
if (entryExpiresCheckBox.isChecked)
setOnDateClickListener?.onClick(it)
}
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle()
// Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
}
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
}
populateViewsWithEntry()
return rootView
}
override fun onDetach() {
super.onDetach()
drawFactory = null
setOnDateClickListener = null
setOnPasswordGeneratorClickListener = null
setOnIconViewClickListener = null
setOnRemoveAttachment = null
setOnEditCustomField = null
}
fun getEntryInfo(): EntryInfo? {
populateEntryWithViews()
return mEntryInfo
}
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
entryPasswordGeneratorView,
{
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
},
{
try {
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
} catch (ignore: Exception) {}
}
)
}
private fun populateViewsWithEntry() {
// Set info in view
icon = mEntryInfo.icon
title = mEntryInfo.title
username = mEntryInfo.username
url = mEntryInfo.url
password = mEntryInfo.password
expires = mEntryInfo.expires
expiryTime = mEntryInfo.expiryTime
notes = mEntryInfo.notes
assignExtraFields(mEntryInfo.customFields) { fields ->
setOnEditCustomField?.invoke(fields)
}
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
setOnRemoveAttachment?.invoke(attachment)
}
}
private fun populateEntryWithViews() {
// Icon already populate
mEntryInfo.title = title
mEntryInfo.username = username
mEntryInfo.url = url
mEntryInfo.password = password
mEntryInfo.expires = expires
mEntryInfo.expiryTime = expiryTime
mEntryInfo.notes = notes
mEntryInfo.customFields = getExtraFields()
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
}?.otpModel
mEntryInfo.attachments = getAttachments()
}
var title: String
get() {
return entryTitleView.text.toString()
}
set(value) {
entryTitleView.setText(value)
if (fontInVisibility)
entryTitleView.applyFontVisibility()
}
var icon: IconImage
get() {
return mEntryInfo.icon
}
set(value) {
mEntryInfo.icon = value
drawFactory?.let { drawFactory ->
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
}
}
var username: String
get() {
return entryUserNameView.text.toString()
}
set(value) {
entryUserNameView.setText(value)
if (fontInVisibility)
entryUserNameView.applyFontVisibility()
}
var url: String
get() {
return entryUrlView.text.toString()
}
set(value) {
entryUrlView.setText(value)
if (fontInVisibility)
entryUrlView.applyFontVisibility()
}
var password: String
get() {
return entryPasswordView.text.toString()
}
set(value) {
entryPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
}
}
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
if (!value) {
expiresInstant = DateInstant.IN_ONE_MONTH
}
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiryTime: DateInstant
get() {
return if (expires)
expiresInstant
else
DateInstant.NEVER_EXPIRE
}
set(value) {
if (expires)
expiresInstant = value
assignExpiresDateText()
}
var notes: String
get() {
return entryNotesView.text.toString()
}
set(value) {
entryNotesView.setText(value)
if (fontInVisibility)
entryNotesView.applyFontVisibility()
}
/* -------------
* Extra Fields
* -------------
*/
private var mExtraFieldsList: MutableList<Field> = ArrayList()
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
private fun buildViewFromField(extraField: Field): View? {
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
itemView?.id = View.NO_ID
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
extraFieldValueContainer?.hint = extraField.name
extraFieldValueContainer?.id = View.NO_ID
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
extraFieldValue?.apply {
if (extraField.protectedValue.isProtected) {
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
}
setText(extraField.protectedValue.toString())
if (fontInVisibility)
applyFontVisibility()
}
extraFieldValue?.id = View.NO_ID
extraFieldValue?.tag = "FIELD_VALUE_TAG"
if (mLastFocusedEditField?.field == extraField) {
mExtraViewToRequestFocus = extraFieldValue
}
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
extraFieldEditButton?.setOnClickListener {
mOnEditButtonClickListener?.invoke(extraField)
}
extraFieldEditButton?.id = View.NO_ID
return itemView
}
fun getExtraFields(): List<Field> {
mLastFocusedEditField = null
for (index in 0 until extraFieldsListView.childCount) {
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
.findViewWithTag("FIELD_VALUE_TAG")
val extraField = mExtraFieldsList[index]
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
if (extraFieldValue.isFocused) {
mLastFocusedEditField = FocusedEditField().apply {
field = extraField
cursorSelectionStart = extraFieldValue.selectionStart
cursorSelectionEnd = extraFieldValue.selectionEnd
}
}
}
return mExtraFieldsList
}
/**
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>,
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)
}
}
mLastFocusedEditField = null
mOnEditButtonClickListener = onEditButtonClickListener
}
/**
* Update an extra field or create a new one if doesn't exists
*/
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
oldField?.let {
val index = mExtraFieldsList.indexOf(oldField)
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, extraField)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
} ?: kotlin.run {
mExtraFieldsList.add(extraField)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView)
newView?.requestFocus()
}
}
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val index = mExtraFieldsList.indexOf(oldExtraField)
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, newExtraField)
extraFieldsListView.removeViewAt(index)
extraFieldsListView.addView(buildViewFromField(newExtraField), index)
}
fun removeExtraField(oldExtraField: Field) {
val previousSize = mExtraFieldsList.size
val index = mExtraFieldsList.indexOf(oldExtraField)
extraFieldsListView.getChildAt(index)?.let {
it.collapse(true) {
mExtraFieldsList.removeAt(index)
extraFieldsListView.removeViewAt(index)
val newSize = mExtraFieldsList.size
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
}
}
}
}
/* -------------
* Attachments
* -------------
*/
fun getAttachments(): List<Attachment> {
return attachmentsAdapter.itemsList.map { it.attachment }
}
fun assignAttachments(attachments: List<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item.attachment)
}
}
fun containsAttachment(): Boolean {
return !attachmentsAdapter.isEmpty()
}
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter.contains(attachment)
}
fun putAttachment(attachment: EntryAttachmentState) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment)
}
fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter.removeItem(attachment)
}
fun clearAttachments() {
attachmentsAdapter.clear()
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({
position.invoke(attachmentsContainerView.y
+ attachmentsListView.y
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
?: 0F)
)
}, 250)
}
override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState)
}
companion object {
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
return EntryEditFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
}
}
}
}
}

View File

@@ -34,6 +34,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.icons.IconPack import com.kunzisoft.keepass.icons.IconPack
@@ -132,7 +133,7 @@ class IconPickerDialogFragment : DialogFragment() {
return bundle.getParcelable(KEY_ICON_STANDARD) return bundle.getParcelable(KEY_ICON_STANDARD)
} }
fun launch(activity: AppCompatActivity) { fun launch(activity: FragmentActivity) {
// Create an instance of the dialog fragment and show it // Create an instance of the dialog fragment and show it
val dialog = IconPickerDialogFragment() val dialog = IconPickerDialogFragment()
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment") dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")

View File

@@ -1,180 +0,0 @@
/*
* 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/>.
*
*/
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.view.EditTextSelectable
import com.kunzisoft.keepass.view.applyFontVisibility
class EntryExtraFieldsItemsAdapter(context: Context)
: AnimatedItemsAdapter<Field, EntryExtraFieldsItemsAdapter.EntryExtraFieldViewHolder>(context) {
var applyFontVisibility = false
set(value) {
field = value
notifyDataSetChanged()
}
private var mValueViewInputType: Int = 0
private var mLastFocusedEditField = FocusedEditField()
private var mLastFocusedTimestamp: Long = 0L
var onEditButtonClickListener: ((item: Field)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryExtraFieldViewHolder {
val view = EntryExtraFieldViewHolder(
inflater.inflate(R.layout.item_entry_edit_extra_field, parent, false)
)
mValueViewInputType = view.extraFieldValue.inputType
return view
}
override fun onBindViewHolder(holder: EntryExtraFieldViewHolder, position: Int) {
val extraField = itemsList[position]
holder.itemView.visibility = View.VISIBLE
if (extraField.protectedValue.isProtected) {
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = true
holder.extraFieldValue.inputType = EditorInfo.TYPE_TEXT_VARIATION_PASSWORD or mValueViewInputType
} else {
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = false
holder.extraFieldValue.inputType = mValueViewInputType
}
holder.extraFieldValueContainer.hint = extraField.name
holder.extraFieldValue.apply {
setText(extraField.protectedValue.toString())
// To Fix focus in RecyclerView
setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setFocusField(extraField, selectionStart, selectionEnd)
} else {
// request focus on last text focused
if (focusedTimestampNotExpired())
requestFocusField(this, extraField, false)
else
removeFocusField(extraField)
}
}
addOnSelectionChangedListener(object: EditTextSelectable.OnSelectionChangedListener {
override fun onSelectionChanged(start: Int, end: Int) {
mLastFocusedEditField.apply {
cursorSelectionStart = start
cursorSelectionEnd = end
}
}
})
requestFocusField(this, extraField, true)
doOnTextChanged { text, _, _, _ ->
extraField.protectedValue.stringValue = text.toString()
}
if (applyFontVisibility)
applyFontVisibility()
}
holder.extraFieldEditButton.setOnClickListener {
onEditButtonClickListener?.invoke(extraField)
}
performDeletion(holder, extraField)
}
fun assignItems(items: List<Field>, focusedEditField: FocusedEditField?) {
focusedEditField?.let {
setFocusField(it, true)
}
super.assignItems(items)
}
private fun setFocusField(field: Field,
selectionStart: Int,
selectionEnd: Int,
force: Boolean = false) {
mLastFocusedEditField.apply {
this.field = field
this.cursorSelectionStart = selectionStart
this.cursorSelectionEnd = selectionEnd
}
setFocusField(mLastFocusedEditField, force)
}
private fun setFocusField(field: FocusedEditField, force: Boolean = false) {
mLastFocusedEditField = field
mLastFocusedTimestamp = if (force) 0L else System.currentTimeMillis()
}
private fun removeFocusField(field: Field? = null) {
if (field == null || mLastFocusedEditField.field == field) {
mLastFocusedEditField.destroy()
}
}
private fun requestFocusField(editText: EditText, field: Field, setSelection: Boolean) {
if (field == mLastFocusedEditField.field) {
editText.apply {
post {
if (setSelection) {
setEditTextSelection(editText)
}
requestFocus()
removeFocusField(field)
}
}
}
}
private fun setEditTextSelection(editText: EditText) {
try {
var newCursorPositionStart = mLastFocusedEditField.cursorSelectionStart
var newCursorPositionEnd = mLastFocusedEditField.cursorSelectionEnd
// Cursor at end if 0 or less
if (newCursorPositionStart < 0 || newCursorPositionEnd < 0) {
newCursorPositionStart = (editText.text?:"").length
newCursorPositionEnd = newCursorPositionStart
}
editText.setSelection(newCursorPositionStart, newCursorPositionEnd)
} catch (ignoredException: Exception) {}
}
private fun focusedTimestampNotExpired(): Boolean {
return mLastFocusedTimestamp == 0L || (mLastFocusedTimestamp + FOCUS_TIMESTAMP) > System.currentTimeMillis()
}
fun getFocusedField(): FocusedEditField {
return mLastFocusedEditField
}
class EntryExtraFieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var extraFieldValueContainer: TextInputLayout = itemView.findViewById(R.id.entry_extra_field_value_container)
var extraFieldValue: EditTextSelectable = itemView.findViewById(R.id.entry_extra_field_value)
var extraFieldEditButton: View = itemView.findViewById(R.id.entry_extra_field_edit)
}
companion object {
// time to focus element when a keyboard appears
private const val FOCUS_TIMESTAMP = 400L
}
}

View File

@@ -834,6 +834,13 @@ class Database {
} }
} }
/**
* @return true if database allows custom field
*/
fun allowEntryCustomFields(): Boolean {
return mDatabaseKDBX != null
}
/** /**
* Remove oldest history for each entry if more than max items or max memory * Remove oldest history for each entry if more than max items or max memory
*/ */

View File

@@ -23,6 +23,8 @@ import android.content.res.Resources
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import org.joda.time.Duration
import org.joda.time.Instant
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
companion object { companion object {
val NEVER_EXPIRE = neverExpire val NEVER_EXPIRE = neverExpire
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
private val dateFormat = SimpleDateFormat.getDateTimeInstance() private val dateFormat = SimpleDateFormat.getDateTimeInstance()
private val neverExpire: DateInstant private val neverExpire: DateInstant

View File

@@ -33,7 +33,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
@@ -284,37 +283,43 @@ class Entry : Node, EntryVersionedInterface<Group> {
} }
/** /**
* Retrieve custom fields to show, key is the label, value is the value of field (protected or not) * Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
* @return Map of label/value * @return Map of label/value
*/ */
val customFields: HashMap<String, ProtectedString> fun getExtraFields(): List<Field> {
get() = entryKDBX?.customFields ?: HashMap() val extraFields = ArrayList<Field>()
entryKDBX?.let {
/** for (field in it.customFields) {
* To redefine if version of entry allow custom field, extraFields.add(Field(field.key, field.value))
* @return true if entry allows custom field }
*/ }
fun allowCustomFields(): Boolean { return extraFields
return entryKDBX?.allowCustomFields() ?: false
}
fun removeAllFields() {
entryKDBX?.removeAllFields()
} }
/** /**
* Update or add an extra field to the list (standard or custom) * Update or add an extra field to the list (standard or custom)
* @param label Label of field, must be unique
* @param value Value of field
*/ */
fun putExtraField(label: String, value: ProtectedString) { fun putExtraField(field: Field) {
entryKDBX?.putExtraField(label, value) entryKDBX?.putExtraField(field.name, field.protectedValue)
}
private fun addExtraFields(fields: List<Field>) {
fields.forEach {
putExtraField(it)
}
}
private fun removeAllFields() {
entryKDBX?.removeAllFields()
} }
fun getOtpElement(): OtpElement? { fun getOtpElement(): OtpElement? {
return OtpEntryFields.parseFields { key -> entryKDBX?.let {
customFields[key]?.toString() return OtpEntryFields.parseFields { key ->
it.customFields[key]?.toString()
}
} }
return null
} }
fun startToManageFieldReferences(database: DatabaseKDBX) { fun startToManageFieldReferences(database: DatabaseKDBX) {
@@ -341,16 +346,27 @@ class Entry : Node, EntryVersionedInterface<Group> {
|| entryKDBX?.containsAttachment() == true || entryKDBX?.containsAttachment() == true
} }
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) { private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
entryKDB?.putAttachment(attachment) attachments.forEach {
entryKDBX?.putAttachment(attachment, binaryPool) putAttachment(it, binaryPool)
}
} }
fun removeAttachment(attachment: Attachment) { private fun removeAttachment(attachment: Attachment) {
entryKDB?.removeAttachment(attachment) entryKDB?.removeAttachment(attachment)
entryKDBX?.removeAttachment(attachment) entryKDBX?.removeAttachment(attachment)
} }
private fun removeAllAttachments() {
entryKDB?.removeAttachment()
entryKDBX?.removeAttachments()
}
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
entryKDB?.putAttachment(attachment)
entryKDBX?.putAttachment(attachment, binaryPool)
}
fun getHistory(): ArrayList<Entry> { fun getHistory(): ArrayList<Entry> {
val history = ArrayList<Entry>() val history = ArrayList<Entry>()
val entryKDBXHistory = entryKDBX?.history ?: ArrayList() val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
@@ -404,26 +420,54 @@ class Entry : Node, EntryVersionedInterface<Group> {
database?.stopManageEntry(this) database?.stopManageEntry(this)
else else
database?.startManageEntry(this) database?.startManageEntry(this)
entryInfo.id = nodeId.toString() entryInfo.id = nodeId.toString()
entryInfo.title = title entryInfo.title = title
entryInfo.icon = icon entryInfo.icon = icon
entryInfo.username = username entryInfo.username = username
entryInfo.password = password entryInfo.password = password
entryInfo.expires = expires
entryInfo.expiryTime = expiryTime
entryInfo.url = url entryInfo.url = url
entryInfo.notes = notes entryInfo.notes = notes
for (entry in customFields.entries) { entryInfo.customFields = getExtraFields()
entryInfo.customFields.add(
Field(entry.key, entry.value))
}
// Add otpElement to generate token // Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel entryInfo.otpModel = getOtpElement()?.otpModel
// Replace parameter fields by generated OTP fields if (!raw) {
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) // Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
}
database?.binaryPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool)
}
if (!raw) if (!raw)
database?.stopManageEntry(this) database?.stopManageEntry(this)
return entryInfo return entryInfo
} }
fun setEntryInfo(database: Database?, newEntryInfo: EntryInfo) {
database?.startManageEntry(this)
removeAllFields()
removeAllAttachments()
// NodeId stay as is
title = newEntryInfo.title
icon = newEntryInfo.icon
username = newEntryInfo.username
password = newEntryInfo.password
expires = newEntryInfo.expires
expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url
notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments)
}
database?.stopManageEntry(this)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

@@ -153,8 +153,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
this.binaryData = attachment.binaryAttachment this.binaryData = attachment.binaryAttachment
} }
fun removeAttachment(attachment: Attachment) { fun removeAttachment(attachment: Attachment? = null) {
if (this.binaryDescription == attachment.name) { if (attachment == null || this.binaryDescription == attachment.name) {
this.binaryDescription = "" this.binaryDescription = ""
this.binaryData = null this.binaryData = null
} }

View File

@@ -38,7 +38,6 @@ import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedLong import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashMap
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface { class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
@@ -272,10 +271,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return field return field
} }
fun allowCustomFields(): Boolean {
return true
}
fun removeAllFields() { fun removeAllFields() {
fields.clear() fields.clear()
} }
@@ -314,6 +309,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
binaries.remove(attachment.name) binaries.remove(attachment.name)
} }
fun removeAttachments() {
binaries.clear()
}
private fun getAttachmentsSize(binaryPool: BinaryPool): Long { private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
var size = 0L var size = 0L
for ((label, poolId) in binaries) { for ((label, poolId) in binaries) {
@@ -323,11 +322,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return size return size
} }
// TODO Remove ?
fun sizeOfHistory(): Int {
return history.size
}
override fun putCustomData(key: String, value: String) { override fun putCustomData(key: String, value: String) {
customData[key] = value customData[key] = value
} }

View File

@@ -21,7 +21,10 @@ package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.* import java.util.*
@@ -30,12 +33,15 @@ class EntryInfo : Parcelable {
var id: String = "" var id: String = ""
var title: String = "" var title: String = ""
var icon: IconImage? = null var icon: IconImage = IconImageStandard()
var username: String = "" var username: String = ""
var password: String = "" var password: String = ""
var expires: Boolean = false
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
var url: String = "" var url: String = ""
var notes: String = "" var notes: String = ""
var customFields: MutableList<Field> = ArrayList() var customFields: List<Field> = ArrayList()
var attachments: List<Attachment> = ArrayList()
var otpModel: OtpModel? = null var otpModel: OtpModel? = null
constructor() constructor()
@@ -43,12 +49,15 @@ class EntryInfo : Parcelable {
private constructor(parcel: Parcel) { private constructor(parcel: Parcel) {
id = parcel.readString() ?: id id = parcel.readString() ?: id
title = parcel.readString() ?: title title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader) icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
username = parcel.readString() ?: username username = parcel.readString() ?: username
password = parcel.readString() ?: password password = parcel.readString() ?: password
expires = parcel.readInt() != 0
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
url = parcel.readString() ?: url url = parcel.readString() ?: url
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
parcel.readList(customFields as List<Field>, Field::class.java.classLoader) parcel.readList(customFields, Field::class.java.classLoader)
parcel.readList(attachments, Attachment::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
} }
@@ -62,9 +71,12 @@ class EntryInfo : Parcelable {
parcel.writeParcelable(icon, flags) parcel.writeParcelable(icon, flags)
parcel.writeString(username) parcel.writeString(username)
parcel.writeString(password) parcel.writeString(password)
parcel.writeInt(if (expires) 1 else 0)
parcel.writeParcelable(expiryTime, flags)
parcel.writeString(url) parcel.writeString(url)
parcel.writeString(notes) parcel.writeString(notes)
parcel.writeArray(customFields.toTypedArray()) parcel.writeArray(customFields.toTypedArray())
parcel.writeArray(attachments.toTypedArray())
parcel.writeParcelable(otpModel, flags) parcel.writeParcelable(otpModel, flags)
} }

View File

@@ -347,7 +347,7 @@ object OtpEntryFields {
* Build new generated fields in a new list from [fieldsToParse] in parameter, * Build new generated fields in a new list from [fieldsToParse] in parameter,
* Remove parameters fields use to generate auto fields * Remove parameters fields use to generate auto fields
*/ */
fun generateAutoFields(fieldsToParse: MutableList<Field>): MutableList<Field> { fun generateAutoFields(fieldsToParse: List<Field>): MutableList<Field> {
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse) val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
// Remove parameter fields // Remove parameter fields
val otpField = Field(OTP_FIELD) val otpField = Field(OTP_FIELD)

View File

@@ -1,42 +0,0 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputEditText
class EditTextSelectable @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: TextInputEditText(context, attrs) {
// TODO constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
// after material design upgrade
private val mOnSelectionChangedListeners: MutableList<OnSelectionChangedListener>?
init {
mOnSelectionChangedListeners = ArrayList()
}
fun addOnSelectionChangedListener(onSelectionChangedListener: OnSelectionChangedListener) {
mOnSelectionChangedListeners?.add(onSelectionChangedListener)
}
fun removeOnSelectionChangedListener(onSelectionChangedListener: OnSelectionChangedListener) {
mOnSelectionChangedListeners?.remove(onSelectionChangedListener)
}
fun removeAllOnSelectionChangedListeners() {
mOnSelectionChangedListeners?.clear()
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
mOnSelectionChangedListeners?.forEach {
it.onSelectionChanged(selStart, selEnd)
}
}
interface OnSelectionChangedListener {
fun onSelectionChanged(start: Int, end: Int)
}
}

View File

@@ -1,359 +0,0 @@
/*
* 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/>.
*
*/
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryExtraFieldsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.model.StreamDirection
import org.joda.time.Duration
import org.joda.time.Instant
class EntryEditContentsView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private var fontInVisibility: Boolean = false
private val entryTitleLayoutView: TextInputLayout
private val entryTitleView: EditText
private val entryIconView: ImageView
private val entryUserNameView: EditText
private val entryUrlView: EditText
private val entryPasswordLayoutView: TextInputLayout
private val entryPasswordView: EditText
val entryPasswordGeneratorView: View
private val entryExpiresCheckBox: CompoundButton
private val entryExpiresTextView: TextView
private val entryNotesView: EditText
private val extraFieldsContainerView: ViewGroup
private val extraFieldsListView: RecyclerView
private val attachmentsContainerView: View
private val attachmentsListView: RecyclerView
private val extraFieldsAdapter = EntryExtraFieldsItemsAdapter(context)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
var onDateClickListener: OnClickListener? = null
set(value) {
field = value
if (entryExpiresCheckBox.isChecked)
entryExpiresTextView.setOnClickListener(value)
else
entryExpiresTextView.setOnClickListener(null)
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_edit_contents, this)
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
entryTitleView = findViewById(R.id.entry_edit_title)
entryIconView = findViewById(R.id.entry_edit_icon_button)
entryUserNameView = findViewById(R.id.entry_edit_user_name)
entryUrlView = findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
entryPasswordView = findViewById(R.id.entry_edit_password)
entryPasswordGeneratorView = findViewById(R.id.entry_edit_password_generator_button)
entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
entryNotesView = findViewById(R.id.entry_edit_notes)
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
extraFieldsListView = findViewById(R.id.extra_fields_list)
// To hide or not the container
extraFieldsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
}
}
extraFieldsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = extraFieldsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list)
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
attachmentsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor.getColor(0, Color.WHITE)
taIconColor.recycle()
}
fun applyFontVisibilityToFields(fontInVisibility: Boolean) {
this.fontInVisibility = fontInVisibility
this.extraFieldsAdapter.applyFontVisibility = fontInVisibility
}
var title: String
get() {
return entryTitleView.text.toString()
}
set(value) {
entryTitleView.setText(value)
if (fontInVisibility)
entryTitleView.applyFontVisibility()
}
fun setDefaultIcon(iconFactory: IconDrawableFactory) {
entryIconView.assignDefaultDatabaseIcon(iconFactory, iconColor)
}
fun setIcon(iconFactory: IconDrawableFactory, icon: IconImage) {
entryIconView.assignDatabaseIcon(iconFactory, icon, iconColor)
}
fun setOnIconViewClickListener(clickListener: () -> Unit) {
entryIconView.setOnClickListener { clickListener.invoke() }
}
var username: String
get() {
return entryUserNameView.text.toString()
}
set(value) {
entryUserNameView.setText(value)
if (fontInVisibility)
entryUserNameView.applyFontVisibility()
}
var url: String
get() {
return entryUrlView.text.toString()
}
set(value) {
entryUrlView.setText(value)
if (fontInVisibility)
entryUrlView.applyFontVisibility()
}
var password: String
get() {
return entryPasswordView.text.toString()
}
set(value) {
entryPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
}
}
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(onDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiresDate: DateInstant
get() {
return expiresInstant
}
set(value) {
expiresInstant = value
assignExpiresDateText()
}
var notes: String
get() {
return entryNotesView.text.toString()
}
set(value) {
entryNotesView.setText(value)
if (fontInVisibility)
entryNotesView.applyFontVisibility()
}
/* -------------
* Extra Fields
* -------------
*/
fun getExtraFields(): List<Field> {
return extraFieldsAdapter.itemsList
}
fun getExtraFieldFocused(): FocusedEditField {
// To keep focused after an orientation change
return extraFieldsAdapter.getFocusedField()
}
/**
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>,
onEditButtonClickListener: ((item: Field)->Unit)?,
focusedExtraField: FocusedEditField? = null) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
extraFieldsAdapter.assignItems(fields, focusedExtraField)
extraFieldsAdapter.onEditButtonClickListener = onEditButtonClickListener
}
/**
* Update an extra field or create a new one if doesn't exists
*/
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = extraFieldsAdapter.itemsList.firstOrNull { it.name == extraField.name }
oldField?.let {
if (extraField.protectedValue.stringValue.isEmpty())
extraField.protectedValue.stringValue = it.protectedValue.stringValue
}
extraFieldsAdapter.putItem(extraField)
}
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
extraFieldsAdapter.replaceItem(oldExtraField, newExtraField)
}
fun removeExtraField(oldExtraField: Field) {
extraFieldsAdapter.removeItem(oldExtraField)
}
fun getExtraFieldViewPosition(field: Field, position: (Float) -> Unit) {
extraFieldsListView.post {
position.invoke(extraFieldsListView.y
+ (extraFieldsListView.getChildAt(extraFieldsAdapter.indexOf(field))?.y
?: 0F)
)
}
}
/* -------------
* Attachments
* -------------
*/
fun getAttachments(): List<Attachment> {
return attachmentsAdapter.itemsList.map { it.attachment }
}
fun assignAttachments(attachments: Set<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item.attachment)
}
}
fun containsAttachment(): Boolean {
return !attachmentsAdapter.isEmpty()
}
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter.contains(attachment)
}
fun putAttachment(attachment: EntryAttachmentState) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment)
}
fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter.removeItem(attachment)
}
fun clearAttachments() {
attachmentsAdapter.clear()
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({
position.invoke(attachmentsContainerView.y
+ attachmentsListView.y
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
?: 0F)
)
}, 250)
}
/**
* Validate or not the entry form
*
* @return ErrorValidation An error with a message or a validation without message
*/
fun isValid(): Boolean {
// TODO
return true
}
}

View File

@@ -54,7 +54,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.kunzisoft.keepass.view.EntryEditContentsView <androidx.fragment.app.FragmentContainerView
android:id="@+id/entry_edit_contents" android:id="@+id/entry_edit_contents"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -209,14 +209,12 @@
android:layout_marginBottom="@dimen/card_view_margin_bottom" android:layout_marginBottom="@dimen/card_view_margin_bottom"
app:layout_constraintTop_toBottomOf="@+id/entry_edit_container" app:layout_constraintTop_toBottomOf="@+id/entry_edit_container"
app:layout_constraintBottom_toTopOf="@+id/entry_attachments_container"> app:layout_constraintBottom_toTopOf="@+id/entry_attachments_container">
<androidx.recyclerview.widget.RecyclerView <LinearLayout
android:id="@+id/extra_fields_list" android:id="@+id/extra_fields_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:descendantFocusability="afterDescendants"
android:layout_margin="@dimen/card_view_padding" android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical"> android:orientation="vertical" />
</androidx.recyclerview.widget.RecyclerView>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView

View File

@@ -34,7 +34,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_extra_field_edit"> app:layout_constraintEnd_toStartOf="@+id/entry_extra_field_edit">
<com.kunzisoft.keepass.view.EditTextSelectable <com.google.android.material.textfield.TextInputEditText
android:id="@+id/entry_extra_field_value" android:id="@+id/entry_extra_field_value"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/edit_text_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/edit_text_show">
<com.kunzisoft.keepass.view.EditTextSelectable
android:id="@+id/edit_text_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:focusable="true"
android:focusableInTouchMode="true"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/edit_text_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_visibility_state"
android:contentDescription="@string/menu_showpass"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -93,8 +93,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding" android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical"> android:orientation="vertical" />
</LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView