mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/Fragment_Edit_Entry' into develop #686
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user