diff --git a/CHANGELOG b/CHANGELOG index e575c1ced..00d67607d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,21 @@ +KeepassDX (2.5.0.0beta22) + * Rebuild code for actions + * Add UUID as entry view + * Fix bug with natural order + * Fix number of entries in databaseV1 + * New entry views + +KeepassDX (2.5.0.0beta21) + * Fix nested groups no longer visible in V1 databases + * Improved data import algorithm for V1 databases + * Add natural database sort + * Add username database sort + * Fix button disabled with only KeyFile + * Show the number of entries in a group + +KeepassDX (2.5.0.0beta20) + * Fix a major bug that displays an entry history + KeepassDX (2.5.0.0beta19) * Add lock button always visible * New connection workflow diff --git a/app/build.gradle b/app/build.gradle index ad8fb759b..a3480f342 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 14 targetSdkVersion 27 - versionCode = 19 - versionName = "2.5.0.0beta19" + versionCode = 22 + versionName = "2.5.0.0beta22" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index 5d014dd38..ec226e9a5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -152,7 +152,7 @@ class EntryActivity : LockingHideActivity() { titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor) // Assign title text - val entryTitle = entry.getVisualTitle() + val entryTitle = entry.title collapsingToolbarLayout?.title = entryTitle toolbar?.title = entryTitle @@ -236,6 +236,9 @@ class EntryActivity : LockingHideActivity() { entryContentsView?.assignExpiresDate(getString(R.string.never)) } + // Assign special data + entryContentsView?.assignUUID(entry.nodeId.id) + database.stopManageEntry(entry) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index e7f4203f4..e85733efd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -54,6 +54,7 @@ import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.SortNodeEnum import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread import com.kunzisoft.keepass.database.action.node.* +import com.kunzisoft.keepass.database.action.node.ActionNodeDatabaseRunnable.Companion.NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.icons.assignDatabaseIcon @@ -61,7 +62,6 @@ import com.kunzisoft.keepass.magikeyboard.KeyboardHelper import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.view.AddNodeButtonView import net.cachapa.expandablelayout.ExpandableLayout @@ -523,10 +523,10 @@ class GroupActivity : LockingActivity(), MoveGroupRunnable( this, Database.getInstance(), - groupToMove, - newParent, - AfterAddNodeRunnable(), - !readOnly) + groupToMove, + newParent, + AfterAddNodeRunnable(), + !readOnly) }.start() } @@ -812,20 +812,30 @@ class GroupActivity : LockingActivity(), override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { runOnUiThread { if (actionNodeValues.result.isSuccess) { - actionNodeValues.oldNode?.let { oldNode -> - mListNodesFragment?.removeNode(oldNode) + // If the action register the position, use it to remove the entry view + val positionNode = actionNodeValues.result.data?.getInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY) + if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB + && positionNode != null) { + mListNodesFragment?.removeNodeAt(positionNode) + } else { + // else use the old Node that was the entry unchanged with the old parent + actionNodeValues.oldNode?.let { oldNode -> + mListNodesFragment?.removeNode(oldNode) + } + } - // TODO Move trash view - // Add trash in views list if it doesn't exists - val database = Database.getInstance() - if (database.isRecycleBinEnabled) { - val recycleBin = database.recycleBin - if (mCurrentGroup != null && recycleBin != null + // Add trash in views list if it doesn't exists + val database = Database.getInstance() + if (database.isRecycleBinEnabled) { + val recycleBin = database.recycleBin + if (mCurrentGroup != null && recycleBin != null && mCurrentGroup!!.parent == null && mCurrentGroup != recycleBin) { - mListNodesFragment?.addNode(recycleBin) - } + if (mListNodesFragment?.contains(recycleBin) == true) + mListNodesFragment?.updateNode(recycleBin) + else + mListNodesFragment?.addNode(recycleBin) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt index b234fec63..618f3bb0b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -25,6 +25,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper +import com.kunzisoft.keepass.database.element.Database class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { @@ -205,24 +206,20 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis R.id.menu_sort -> { context?.let { context -> - val sortDialogFragment: SortDialogFragment - - /* - // TODO Recycle bin bottom - if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) { - sortDialogFragment = + val sortDialogFragment: SortDialogFragment = + if (Database.getInstance().isRecycleBinAvailable + && Database.getInstance().isRecycleBinEnabled) { SortDialogFragment.getInstance( - PrefsUtil.getListSort(this), - PrefsUtil.getAscendingSort(this), - PrefsUtil.getGroupsBeforeSort(this), - PrefsUtil.getRecycleBinBottomSort(this)); - } else { - */ - sortDialogFragment = SortDialogFragment.getInstance( - PreferencesUtil.getListSort(context), - PreferencesUtil.getAscendingSort(context), - PreferencesUtil.getGroupsBeforeSort(context)) - //} + PreferencesUtil.getListSort(context), + PreferencesUtil.getAscendingSort(context), + PreferencesUtil.getGroupsBeforeSort(context), + PreferencesUtil.getRecycleBinBottomSort(context)) + } else { + SortDialogFragment.getInstance( + PreferencesUtil.getListSort(context), + PreferencesUtil.getAscendingSort(context), + PreferencesUtil.getGroupsBeforeSort(context)) + } sortDialogFragment.show(childFragmentManager, "sortDialog") } @@ -255,18 +252,26 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } } + fun contains(node: NodeVersioned): Boolean { + return mAdapter?.contains(node) ?: false + } + fun addNode(newNode: NodeVersioned) { mAdapter?.addNode(newNode) } - fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) { - mAdapter?.updateNode(oldNode, newNode) + fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) { + mAdapter?.updateNode(oldNode, newNode ?: oldNode) } fun removeNode(pwNode: NodeVersioned) { mAdapter?.removeNode(pwNode) } + fun removeNodeAt(position: Int) { + mAdapter?.removeNodeAt(position) + } + interface OnScrollListener { /** diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index 14dbe41ac..638e99320 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -48,7 +48,6 @@ import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.helpers.* import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity -import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable import com.kunzisoft.keepass.database.action.ProgressDialogThread @@ -148,10 +147,8 @@ class PasswordActivity : StylishActivity(), } }) - enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> - if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { - confirmButtonView?.isEnabled = isChecked - } + enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ -> + enableOrNotTheConfirmationButton() } } @@ -175,15 +172,6 @@ class PasswordActivity : StylishActivity(), // For check shutdown super.onResume() - // Enable or not the open button - if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { - checkboxPasswordView?.let { - confirmButtonView?.isEnabled = it.isChecked - } - } else { - confirmButtonView?.isEnabled = true - } - UriIntentInitTask(WeakReference(this), this, mRememberKeyFile) .execute(intent) } @@ -288,6 +276,21 @@ class PasswordActivity : StylishActivity(), if (!fingerPrintInit) { checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener) } + checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener) + } + + enableOrNotTheConfirmationButton() + } + + private fun enableOrNotTheConfirmationButton() { + // Enable or not the open button if setting is checked + if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { + checkboxPasswordView?.let { + confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true + || checkboxKeyFileView?.isChecked == true) + } + } else { + confirmButtonView?.isEnabled = true } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt index b7d0859db..0c823e862 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt @@ -35,12 +35,14 @@ class SortDialogFragment : DialogFragment() { private var mListener: SortSelectionListener? = null - private var mSortNodeEnum: SortNodeEnum? = null + private var mSortNodeEnum: SortNodeEnum = SortNodeEnum.DB @IdRes private var mCheckedId: Int = 0 - private var mGroupsBefore: Boolean = false - private var mAscending: Boolean = false - private var mRecycleBinBottom: Boolean = false + private var mGroupsBefore: Boolean = true + private var mAscending: Boolean = true + private var mRecycleBinBottom: Boolean = true + + private var recycleBinBottomView: CompoundButton? = null override fun onAttach(context: Context?) { super.onAttach(context) @@ -50,18 +52,13 @@ class SortDialogFragment : DialogFragment() { throw ClassCastException(context!!.toString() + " must implement " + SortSelectionListener::class.java.name) } - } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { activity?.let { activity -> val builder = AlertDialog.Builder(activity) - mSortNodeEnum = SortNodeEnum.TITLE - mAscending = true - mGroupsBefore = true var recycleBinAllowed = false - mRecycleBinBottom = true arguments?.apply { if (containsKey(SORT_NODE_ENUM_BUNDLE_KEY)) @@ -78,14 +75,14 @@ class SortDialogFragment : DialogFragment() { } } - mCheckedId = retrieveViewFromEnum(mSortNodeEnum!!) + mCheckedId = retrieveViewFromEnum(mSortNodeEnum) val rootView = activity.layoutInflater.inflate(R.layout.fragment_sort_selection, null) builder.setTitle(R.string.sort_menu) builder.setView(rootView) // Add action buttons .setPositiveButton(android.R.string.ok - ) { _, _ -> mListener?.onSortSelected(mSortNodeEnum!!, mAscending, mGroupsBefore, mRecycleBinBottom) } + ) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) } .setNegativeButton(R.string.cancel) { _, _ -> } val ascendingView = rootView.findViewById(R.id.sort_selection_ascending) @@ -98,25 +95,35 @@ class SortDialogFragment : DialogFragment() { groupsBeforeView.isChecked = mGroupsBefore groupsBeforeView.setOnCheckedChangeListener { _, isChecked -> mGroupsBefore = isChecked } - val recycleBinBottomView = rootView.findViewById(R.id.sort_selection_recycle_bin_bottom) + recycleBinBottomView = rootView.findViewById(R.id.sort_selection_recycle_bin_bottom) if (!recycleBinAllowed) { - recycleBinBottomView.visibility = View.GONE + recycleBinBottomView?.visibility = View.GONE } else { // Check if recycle bin at the bottom - recycleBinBottomView.isChecked = mRecycleBinBottom - recycleBinBottomView.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked } + recycleBinBottomView?.isChecked = mRecycleBinBottom + recycleBinBottomView?.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked } + + disableRecycleBinBottomOptionIfNaturalOrder() } val sortSelectionRadioGroupView = rootView.findViewById(R.id.sort_selection_radio_group) // Check value by default sortSelectionRadioGroupView.check(mCheckedId) - sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId -> mSortNodeEnum = retrieveSortEnumFromViewId(checkedId) } + sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId -> + mSortNodeEnum = retrieveSortEnumFromViewId(checkedId) + disableRecycleBinBottomOptionIfNaturalOrder() + } return builder.create() } return super.onCreateDialog(savedInstanceState) } + private fun disableRecycleBinBottomOptionIfNaturalOrder() { + // Disable recycle bin if natural order + recycleBinBottomView?.isEnabled = mSortNodeEnum != SortNodeEnum.DB + } + @IdRes private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int { return when (sortNodeEnum) { diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt index 3053fe500..581691bd8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -30,7 +30,6 @@ import android.widget.ImageView import android.widget.TextView import android.widget.Toast import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.database.SortNodeEnum import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.icons.assignDatabaseIcon @@ -48,11 +47,14 @@ class NodeAdapter private val inflater: LayoutInflater = LayoutInflater.from(context) private var textSize: Float = 0.toFloat() private var subtextSize: Float = 0.toFloat() + private var infoTextSize: Float = 0.toFloat() private var iconSize: Float = 0.toFloat() - private var listSort: SortNodeEnum? = null - private var groupsBeforeSort: Boolean = false - private var ascendingSort: Boolean = false - private var showUserNames: Boolean = false + private var listSort: SortNodeEnum = SortNodeEnum.DB + private var ascendingSort: Boolean = true + private var groupsBeforeSort: Boolean = true + private var recycleBinBottomSort: Boolean = true + private var showUserNames: Boolean = true + private var showNumberEntries: Boolean = true private var nodeClickCallback: NodeClickCallback? = null private var nodeMenuListener: NodeMenuListener? = null @@ -80,11 +82,13 @@ class NodeAdapter this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback(this) { override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int { - return listSort?.getNodeComparator(ascendingSort, groupsBeforeSort)?.compare(item1, item2) ?: 0 + return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2) } override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean { - return oldItem.title == newItem.title && oldItem.icon == newItem.icon + return oldItem.type == newItem.type + && oldItem.title == newItem.title + && oldItem.icon == newItem.icon } override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean { @@ -121,13 +125,16 @@ class NodeAdapter val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default)) this.textSize = PreferencesUtil.getListTextSize(context) this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault + this.infoTextSize = context.resources.getInteger(R.integer.list_tiny_size_default) * textSize / textSizeDefault // Retrieve the icon size val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default) this.iconSize = iconDefaultSize * textSize / textSizeDefault this.listSort = PreferencesUtil.getListSort(context) - this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context) this.ascendingSort = PreferencesUtil.getAscendingSort(context) + this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context) + this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context) this.showUserNames = PreferencesUtil.showUsernamesListEntries(context) + this.showNumberEntries = PreferencesUtil.showNumberEntries(context) } /** @@ -136,14 +143,16 @@ class NodeAdapter fun rebuildList(group: GroupVersioned) { this.nodeSortedList.clear() assignPreferences() - // TODO verify sort try { - this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream()) + this.nodeSortedList.addAll(group.getChildren()) } catch (e: Exception) { Log.e(TAG, "Can't add node elements to the list", e) Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show() } + } + fun contains(node: NodeVersioned): Boolean { + return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION } /** @@ -162,6 +171,13 @@ class NodeAdapter nodeSortedList.remove(node) } + /** + * Remove a node at [position] in the list + */ + fun removeNodeAt(position: Int) { + nodeSortedList.removeItemAt(position) + } + /** * Update a node in the list * @param oldNode Node before the update @@ -239,6 +255,17 @@ class NodeAdapter holder.icon.layoutParams?.width = iconSize.toInt() holder.text.textSize = textSize holder.subText.textSize = subtextSize + if (subNode.type == Type.GROUP) { + if (showNumberEntries) { + holder.numberChildren?.apply { + text = (subNode as GroupVersioned).getChildEntries(true).size.toString() + textSize = infoTextSize + visibility = View.VISIBLE + } + } else { + holder.numberChildren?.visibility = View.GONE + } + } } override fun getItemCount(): Int { @@ -356,6 +383,7 @@ class NodeAdapter var icon: ImageView = itemView.findViewById(R.id.node_icon) var text: TextView = itemView.findViewById(R.id.node_text) var subText: TextView = itemView.findViewById(R.id.node_subtext) + var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) } companion object { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt index 22a6b2415..87d2832c2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt @@ -20,41 +20,54 @@ package com.kunzisoft.keepass.database +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.EntryVersioned -import com.kunzisoft.keepass.database.element.GroupVersioned import com.kunzisoft.keepass.database.element.NodeVersioned - -import java.util.Comparator +import com.kunzisoft.keepass.database.element.Type +import java.util.* enum class SortNodeEnum { DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME; - fun getNodeComparator(ascending: Boolean, groupsBefore: Boolean): Comparator { + fun getNodeComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean): Comparator { return when (this) { - DB -> NodeCreationComparator(ascending, groupsBefore) // TODO Sort - TITLE -> NodeTitleComparator(ascending, groupsBefore) - USERNAME -> NodeCreationComparator(ascending, groupsBefore) // TODO Sort - CREATION_TIME -> NodeCreationComparator(ascending, groupsBefore) - LAST_MODIFY_TIME -> NodeLastModificationComparator(ascending, groupsBefore) - LAST_ACCESS_TIME -> NodeLastAccessComparator(ascending, groupsBefore) + DB -> NodeNaturalComparator(ascending, groupsBefore, false) // Force false because natural order contains recycle bin + TITLE -> NodeTitleComparator(ascending, groupsBefore, recycleBinBottom) + USERNAME -> NodeUsernameComparator(ascending, groupsBefore, recycleBinBottom) + CREATION_TIME -> NodeCreationComparator(ascending, groupsBefore, recycleBinBottom) + LAST_MODIFY_TIME -> NodeLastModificationComparator(ascending, groupsBefore, recycleBinBottom) + LAST_ACCESS_TIME -> NodeLastAccessComparator(ascending, groupsBefore, recycleBinBottom) } } - abstract class NodeComparator internal constructor(internal var ascending: Boolean, private var groupsBefore: Boolean) : Comparator { + abstract class NodeComparator(var ascending: Boolean, var groupsBefore: Boolean, var recycleBinBottom: Boolean) : Comparator { - internal fun compareWith(comparatorGroup: Comparator, - comparatorEntry: Comparator, - object1: NodeVersioned, - object2: NodeVersioned, - resultOfNodeMethodCompare: Int): Int { + abstract fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int + + private fun specificOrderOrHashIfEquals(object1: NodeVersioned, object2: NodeVersioned): Int { + val specificOrderComp = compareBySpecificOrder(object1, object2) + + return if (specificOrderComp == 0) { + object1.hashCode() - object2.hashCode() + } else if (!ascending) -specificOrderComp else specificOrderComp // If descending, revert + } + + override fun compare(object1: NodeVersioned,object2: NodeVersioned): Int { if (object1 == object2) return 0 - if (object1 is GroupVersioned) { - return if (object2 is GroupVersioned) { - comparatorGroup - .compare(object1, object2) - } else if (object2 is EntryVersioned) { + if (object1.type == Type.GROUP) { + return if (object2.type == Type.GROUP) { + // RecycleBin at end of groups + if (recycleBinBottom) { + if (Database.getInstance().recycleBin == object1) + return 1 + if (Database.getInstance().recycleBin == object2) + return -1 + } + + specificOrderOrHashIfEquals(object1, object2) + } else if (object2.type == Type.ENTRY) { if (groupsBefore) -1 else @@ -62,11 +75,10 @@ enum class SortNodeEnum { } else { -1 } - } else if (object1 is EntryVersioned) { - return if (object2 is EntryVersioned) { - comparatorEntry - .compare(object1, object2) - } else if (object2 is GroupVersioned) { + } else if (object1.type == Type.ENTRY) { + return if (object2.type == Type.ENTRY) { + specificOrderOrHashIfEquals(object1, object2) + } else if (object2.type == Type.GROUP) { if (groupsBefore) 1 else @@ -76,235 +88,83 @@ enum class SortNodeEnum { } } - // If same name, can be different - return if (resultOfNodeMethodCompare == 0) object1.hashCode() - object2.hashCode() else resultOfNodeMethodCompare + // Type not known + return -1 } } /** - * Comparator of Node by Title, Groups first, Entries second + * Comparator of node by natural database placement */ - class NodeTitleComparator internal constructor(ascending: Boolean, groupsBefore: Boolean) : NodeComparator(ascending, groupsBefore) { + class NodeNaturalComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) + : NodeComparator(ascending, groupsBefore, recycleBinBottom) { - override fun compare(object1: NodeVersioned, object2: NodeVersioned): Int { - - return compareWith( - GroupNameComparator(ascending), - EntryNameComparator(ascending), - object1, - object2, - object1.title - .compareTo(object2.title, ignoreCase = true)) + override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { + return object1.nodePositionInParent.compareTo(object2.nodePositionInParent) } } /** - * Comparator of node by creation, Groups first, Entries second + * Comparator of Node by Title */ - class NodeCreationComparator internal constructor(ascending: Boolean, groupsBefore: Boolean) : NodeComparator(ascending, groupsBefore) { + class NodeTitleComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) + : NodeComparator(ascending, groupsBefore, recycleBinBottom) { - override fun compare(object1: NodeVersioned, object2: NodeVersioned): Int { - - return compareWith( - GroupCreationComparator(ascending), - EntryCreationComparator(ascending), - object1, - object2, - object1.creationTime.date - ?.compareTo(object2.creationTime.date) ?: 0) + override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { + return object1.title.compareTo(object2.title, ignoreCase = true) } } /** - * Comparator of node by last modification, Groups first, Entries second + * Comparator of Node by Username, Groups by title */ - class NodeLastModificationComparator internal constructor(ascending: Boolean, groupsBefore: Boolean) : NodeComparator(ascending, groupsBefore) { + class NodeUsernameComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) + : NodeComparator(ascending, groupsBefore, recycleBinBottom) { - override fun compare(object1: NodeVersioned, object2: NodeVersioned): Int { - - return compareWith( - GroupLastModificationComparator(ascending), - EntryLastModificationComparator(ascending), - object1, - object2, - object1.lastModificationTime.date - ?.compareTo(object2.lastModificationTime.date) ?: 0) + override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { + if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) { + // To get username if it's a ref + return (object1 as EntryVersioned).getEntryInfo(Database.getInstance()).username + .compareTo((object2 as EntryVersioned).getEntryInfo(Database.getInstance()).username, + ignoreCase = true) + } + return NodeTitleComparator(ascending, groupsBefore, recycleBinBottom).compare(object1, object2) } } /** - * Comparator of node by last access, Groups first, Entries second + * Comparator of node by creation */ - class NodeLastAccessComparator internal constructor(ascending: Boolean, groupsBefore: Boolean) : NodeComparator(ascending, groupsBefore) { + class NodeCreationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) + : NodeComparator(ascending, groupsBefore, recycleBinBottom) { - override fun compare(object1: NodeVersioned, object2: NodeVersioned): Int { - - return compareWith( - GroupLastAccessComparator(ascending), - EntryLastAccessComparator(ascending), - object1, - object2, - object1.lastAccessTime.date - ?.compareTo(object2.lastAccessTime.date) ?: 0) - } - } - - abstract class AscendingComparator internal constructor(private val ascending: Boolean) : Comparator { - - internal fun compareWithAscending(basicCompareResult: Int): Int { - // If descending, revert - return if (!ascending) -basicCompareResult else basicCompareResult - - } - } - - /** - * Group comparator by name - */ - class GroupNameComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { - - override fun compare(object1: GroupVersioned, object2: GroupVersioned): Int { - if (object1 == object2) - return 0 - - val groupNameComp = object1.title.compareTo(object2.title, ignoreCase = true) - // If same name, can be different - return if (groupNameComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(groupNameComp) - - } - } - - /** - * Group comparator by name - */ - class GroupCreationComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { - - override fun compare(object1: GroupVersioned, object2: GroupVersioned): Int { - if (object1 == object2) - return 0 - - val groupCreationComp = object1.creationTime.date + override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { + return object1.creationTime.date ?.compareTo(object2.creationTime.date) ?: 0 - // If same creation, can be different - return if (groupCreationComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(groupCreationComp) - } } /** - * Group comparator by last modification + * Comparator of node by last modification */ - class GroupLastModificationComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { + class NodeLastModificationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) + : NodeComparator(ascending, groupsBefore, recycleBinBottom) { - override fun compare(object1: GroupVersioned, object2: GroupVersioned): Int { - if (object1 == object2) - return 0 - - val groupLastModificationComp = object1.lastModificationTime.date + override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { + return object1.lastModificationTime.date ?.compareTo(object2.lastModificationTime.date) ?: 0 - // If same creation, can be different - return if (groupLastModificationComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(groupLastModificationComp) - } } /** - * Group comparator by last access + * Comparator of node by last access */ - class GroupLastAccessComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { + class NodeLastAccessComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) + : NodeComparator(ascending, groupsBefore, recycleBinBottom) { - override fun compare(object1: GroupVersioned, object2: GroupVersioned): Int { - if (object1 == object2) - return 0 - - val groupLastAccessComp = object1.lastAccessTime.date + override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { + return object1.lastAccessTime.date ?.compareTo(object2.lastAccessTime.date) ?: 0 - // If same creation, can be different - return if (groupLastAccessComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(groupLastAccessComp) - - } - } - - /** - * Comparator of Entry by Name - */ - class EntryNameComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { - - override fun compare(object1: EntryVersioned, object2: EntryVersioned): Int { - if (object1 == object2) - return 0 - - val entryTitleComp = object1.title.compareTo(object2.title, ignoreCase = true) - // If same title, can be different - return if (entryTitleComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(entryTitleComp) - - } - } - - /** - * Comparator of Entry by Creation - */ - class EntryCreationComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { - - override fun compare(object1: EntryVersioned, object2: EntryVersioned): Int { - if (object1 == object2) - return 0 - - val entryCreationComp = object1.creationTime.date - ?.compareTo(object2.creationTime.date) ?: 0 - // If same creation, can be different - return if (entryCreationComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(entryCreationComp) - - } - } - - /** - * Comparator of Entry by Last Modification - */ - class EntryLastModificationComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { - - override fun compare(object1: EntryVersioned, object2: EntryVersioned): Int { - if (object1 == object2) - return 0 - - val entryLastModificationComp = object1.lastModificationTime.date - ?.compareTo(object2.lastModificationTime.date) ?: 0 - // If same creation, can be different - return if (entryLastModificationComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(entryLastModificationComp) - - } - } - - /** - * Comparator of Entry by Last Access - */ - class EntryLastAccessComparator internal constructor(ascending: Boolean) : AscendingComparator(ascending) { - - override fun compare(object1: EntryVersioned, object2: EntryVersioned): Int { - if (object1 == object2) - return 0 - - val entryLastAccessComp = object1.lastAccessTime.date - ?.compareTo(object2.lastAccessTime.date) ?: 0 - // If same creation, can be different - return if (entryLastAccessComp == 0) { - object1.hashCode() - object2.hashCode() - } else compareWithAscending(entryLastAccessComp) - } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt index b4e286f21..4ba6b266d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt @@ -1,6 +1,7 @@ package com.kunzisoft.keepass.database.action.node import android.support.v4.app.FragmentActivity +import android.util.Log import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.element.Database @@ -23,6 +24,7 @@ abstract class ActionNodeDatabaseRunnable( super.run() finishRun(true) } catch (e: Exception) { + Log.e("ActionNodeDBRunnable", e.message) finishRun(false, e.message) } } @@ -43,4 +45,8 @@ abstract class ActionNodeDatabaseRunnable( super.onFinishRun(result) } + + companion object { + const val NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY = "NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY" + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt index 830f36b5d..cdcba872f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt @@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.action.node import android.support.v4.app.FragmentActivity import android.util.Log +import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.EntryVersioned import com.kunzisoft.keepass.database.element.GroupVersioned @@ -37,9 +38,18 @@ class CopyEntryRunnable constructor( private var mEntryCopied: EntryVersioned? = null override fun nodeAction() { - // Update entry with new values - mNewParent.touch(modified = false, touchParents = true) - mEntryCopied = database.copyEntryTo(mEntryToCopy, mNewParent) + // Condition + var conditionAccepted = true + if(mNewParent == database.rootGroup && !database.rootCanContainsEntry()) + conditionAccepted = false + if (conditionAccepted) { + // Update entry with new values + mNewParent.touch(modified = false, touchParents = true) + mEntryCopied = database.copyEntryTo(mEntryToCopy, mNewParent) + } else { + // Only finish thread + throw Exception(context.getString(R.string.error_copy_entry_here)) + } mEntryCopied?.apply { touch(modified = true, touchParents = true) @@ -56,7 +66,6 @@ class CopyEntryRunnable constructor( } catch (e: Exception) { Log.i(TAG, "Unable to delete the copied entry") } - } return ActionNodeValues(result, mEntryToCopy, mEntryCopied) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt index 2069a182c..49891915e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.database.action.node +import android.os.Bundle import android.support.v4.app.FragmentActivity import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.EntryVersioned @@ -35,11 +36,19 @@ class DeleteEntryRunnable constructor( private var mParent: GroupVersioned? = null private var mCanRecycle: Boolean = false + private var mEntryToDeleteBackup: EntryVersioned? = null + private var mNodePosition: Int? = null override fun nodeAction() { mParent = mEntryToDelete.parent mParent?.touch(modified = false, touchParents = true) + // Get the node position + mNodePosition = mEntryToDelete.nodePositionInParent + + // Create a copy to keep the old ref and remove it visually + mEntryToDeleteBackup = EntryVersioned(mEntryToDelete) + // Remove Entry from parent mCanRecycle = database.canRecycle(mEntryToDelete) if (mCanRecycle) { @@ -59,6 +68,16 @@ class DeleteEntryRunnable constructor( } } } - return ActionNodeValues(result, mEntryToDelete, null) + + // Add position in bundle to delete the node in view + mNodePosition?.let { position -> + result.data = Bundle().apply { + putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position ) + } + } + + // Return a copy of unchanged entry as old param + // and entry deleted or moved in recycle bin as new param + return ActionNodeValues(result, mEntryToDeleteBackup, mEntryToDelete) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt index a38857583..bdf3ba036 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.database.action.node +import android.os.Bundle import android.support.v4.app.FragmentActivity import com.kunzisoft.keepass.database.element.Database @@ -32,10 +33,19 @@ class DeleteGroupRunnable(context: FragmentActivity, private var mParent: GroupVersioned? = null private var mRecycle: Boolean = false + private var mGroupToDeleteBackup: GroupVersioned? = null + private var mNodePosition: Int? = null + override fun nodeAction() { mParent = mGroupToDelete.parent mParent?.touch(modified = false, touchParents = true) + // Get the node position + mNodePosition = mGroupToDelete.nodePositionInParent + + // Create a copy to keep the old ref and remove it visually + mGroupToDeleteBackup = GroupVersioned(mGroupToDelete) + // Remove Group from parent mRecycle = database.canRecycle(mGroupToDelete) if (mRecycle) { @@ -56,6 +66,16 @@ class DeleteGroupRunnable(context: FragmentActivity, // TODO database.undoDeleteGroupFrom(mGroup, mParent); } } - return ActionNodeValues(result, mGroupToDelete, null) + + // Add position in bundle to delete the node in view + mNodePosition?.let { position -> + result.data = Bundle().apply { + putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position ) + } + } + + // Return a copy of unchanged group as old param + // and group deleted or moved in recycle bin as new param + return ActionNodeValues(result, mGroupToDeleteBackup, mGroupToDelete) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt index 50b375eaa..ac226e1c9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt @@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.action.node import android.support.v4.app.FragmentActivity import android.util.Log +import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.EntryVersioned import com.kunzisoft.keepass.database.element.GroupVersioned @@ -40,7 +41,18 @@ class MoveEntryRunnable constructor( // Move entry in new parent mEntryToMove?.let { mOldParent = it.parent - database.moveEntryTo(it, mNewParent) + + // Condition + var conditionAccepted = true + if(mNewParent == database.rootGroup && !database.rootCanContainsEntry()) + conditionAccepted = false + // Move only if the parent change + if (mOldParent != mNewParent && conditionAccepted) { + database.moveEntryTo(it, mNewParent) + } else { + // Only finish thread + throw Exception(context.getString(R.string.error_move_entry_here)) + } it.touch(modified = true, touchParents = true) } ?: Log.e(TAG, "Unable to create a copy of the entry") } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt index d084a56e5..6dd02b8f5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt @@ -46,9 +46,7 @@ class MoveGroupRunnable constructor( finishRun(true) } else { // Only finish thread - val message = context.getString(R.string.error_move_folder_in_itself) - Log.e(TAG, message) - finishRun(false, message) + throw Exception(context.getString(R.string.error_move_folder_in_itself)) } } ?: Log.e(TAG, "Unable to create a copy of the group") } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 5dbaaba9e..8954abf88 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -304,14 +304,12 @@ class Database { val searchResult = search(query, SearchDbHelper.MAX_SEARCH_ENTRY) if (searchResult != null) { - for (entry in searchResult.getChildEntries()) { - if (!entry.isMetaStream) { // TODO metastream - entry.pwEntryV3?.let { - cursorV3?.addEntry(it) - } - entry.pwEntryV4?.let { - cursorV4?.addEntry(it) - } + for (entry in searchResult.getChildEntries(true)) { + entry.pwEntryV3?.let { + cursorV3?.addEntry(it) + } + entry.pwEntryV4?.let { + cursorV4?.addEntry(it) } } } @@ -484,6 +482,10 @@ class Database { pwDatabaseV4?.retrieveMasterKey(key, keyInputStream) } + fun rootCanContainsEntry(): Boolean { + return pwDatabaseV3?.rootCanContainsEntry() ?: pwDatabaseV4?.rootCanContainsEntry() ?: false + } + fun createEntry(): EntryVersioned? { pwDatabaseV3?.let { database -> return EntryVersioned(database.createEntry()).apply { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt index bd3018318..7eb93df04 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt @@ -338,11 +338,24 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { return entryInfo } - /* - ------------ - Class methods - ------------ - */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EntryVersioned + + if (pwEntryV3 != other.pwEntryV3) return false + if (pwEntryV4 != other.pwEntryV4) return false + + return true + } + + override fun hashCode(): Int { + var result = pwEntryV3?.hashCode() ?: 0 + result = 31 * result + (pwEntryV4?.hashCode() ?: 0) + return result + } + companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): EntryVersioned { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt index 8ee77d8f5..3673e9cfb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt @@ -179,10 +179,16 @@ class GroupVersioned : NodeVersioned, PwGroupInterface { + return getChildEntries(false) + } + + fun getChildEntries(withoutMetaStream: Boolean): MutableList { val children = ArrayList() pwGroupV3?.getChildEntries()?.forEach { - children.add(EntryVersioned(it)) + val entryToAddAsChild = EntryVersioned(it) + if (!withoutMetaStream || (withoutMetaStream && !entryToAddAsChild.isMetaStream)) + children.add(entryToAddAsChild) } pwGroupV4?.getChildEntries()?.forEach { children.add(EntryVersioned(it)) @@ -195,16 +201,16 @@ class GroupVersioned : NodeVersioned, PwGroupInterface { + fun getChildren(withoutMetaStream: Boolean = true): List { val children = ArrayList() children.addAll(getChildGroups()) pwGroupV3?.let { - children.addAll(getChildEntries().filter { !it.isMetaStream }) + children.addAll(getChildEntries(withoutMetaStream)) } pwGroupV4?.let { // No MetasStream in V4 - children.addAll(getChildEntries()) + children.addAll(getChildEntries(withoutMetaStream)) } return children diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt index c0de233e9..8d6759f15 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt @@ -1,6 +1,18 @@ package com.kunzisoft.keepass.database.element -interface NodeVersioned: PwNodeInterface +interface NodeVersioned: PwNodeInterface { + + val nodePositionInParent: Int + get() { + parent?.getChildren(true)?.let { children -> + for ((i, child) in children.withIndex()) { + if (child == this) + return i + } + } + return -1 + } +} /** * Type of available Nodes diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt index e514e5f5b..583005d4a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.database.element +import android.util.Log import com.kunzisoft.keepass.database.exception.InvalidKeyFileException import com.kunzisoft.keepass.database.exception.KeyFileEmptyException import com.kunzisoft.keepass.utils.MemUtil @@ -233,7 +234,12 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry@phoneid.org> @@ -76,53 +75,6 @@ class PwDatabaseV3 : PwDatabase() { numKeyEncRounds = DEFAULT_ENCRYPTION_ROUNDS } - private fun assignGroupsChildren(parent: PwGroupV3) { - val levelToCheck = parent.level + 1 - var startFromParentPosition = false - for (groupToCheck in getGroupIndexes()) { - rootGroup?.let { root -> - if (root.nodeId == parent.nodeId || groupToCheck.nodeId == parent.nodeId) { - startFromParentPosition = true - } - } - if (startFromParentPosition) { - if (groupToCheck.level < levelToCheck) - break - else if (groupToCheck.level == levelToCheck) - parent.addChildGroup(groupToCheck) - } - } - } - - private fun assignEntriesChildren(parent: PwGroupV3) { - for (entry in getEntryIndexes()) { - if (entry.parent!!.nodeId == parent.nodeId) - parent.addChildEntry(entry) - } - } - - private fun constructTreeFromIndex(currentGroup: PwGroupV3) { - - assignGroupsChildren(currentGroup) - assignEntriesChildren(currentGroup) - - // set parent in child entries (normally useless but to be sure or to update parent metadata) - for (childEntry in currentGroup.getChildEntries()) { - childEntry.parent = currentGroup - } - // recursively construct child groups - for (childGroup in currentGroup.getChildGroups()) { - childGroup.parent = currentGroup - constructTreeFromIndex(childGroup) - } - } - - fun constructTreeFromIndex() { - rootGroup?.let { - constructTreeFromIndex(it) - } - } - /** * Generates an unused random tree id * @@ -198,6 +150,10 @@ class PwDatabaseV3 : PwDatabase() { return PwEntryV3() } + override fun rootCanContainsEntry(): Boolean { + return false + } + override fun isBackup(group: PwGroupV3): Boolean { var currentGroup: PwGroupV3? = group while (currentGroup != null) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt index d343b05ac..6c864457f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt @@ -317,6 +317,10 @@ class PwDatabaseV4 : PwDatabase { return PwEntryV4() } + override fun rootCanContainsEntry(): Boolean { + return true + } + override fun isBackup(group: PwGroupV4): Boolean { if (recycleBin == null) return false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt index ac285c007..9764173eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt @@ -268,10 +268,6 @@ class PwEntryV4 : PwEntry, NodeV4Interface { binaries[key] = value } - fun addToHistory(entry: PwEntryV4) { - history.add(entry) - } - fun sizeOfHistory(): Int { return history.size } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt index 2a0ccf13e..8153fd75b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt @@ -1,7 +1,6 @@ package com.kunzisoft.keepass.database.element import android.os.Parcel -import java.util.* abstract class PwGroup < @@ -31,6 +30,8 @@ abstract class PwGroup protected fun updateWith(source: PwGroup) { super.updateWith(source) titleGroup = source.titleGroup + childGroups.addAll(source.childGroups) + childEntries.addAll(source.childEntries) } override var title: String @@ -46,10 +47,14 @@ abstract class PwGroup } override fun addChildGroup(group: Group) { + if (childGroups.contains(group)) + removeChildGroup(group) this.childGroups.add(group) } override fun addChildEntry(entry: Entry) { + if (childEntries.contains(entry)) + removeChildEntry(entry) this.childEntries.add(entry) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt index 2c72ce9dc..3771f2d09 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt @@ -68,9 +68,6 @@ import java.util.Arrays /** * Load a v3 database file. - * - * @author Naomaru Itoi @phoneid.org> - * @author Bill Zwicky @pobox.com> */ class ImporterV3 : Importer() { @@ -223,11 +220,52 @@ class ImporterV3 : Importer() { pos += 2 + 4 + fieldSize } - mDatabaseToOpen.constructTreeFromIndex() + constructTreeFromIndex() return mDatabaseToOpen } + private fun buildTreeGroups(previousGroup: PwGroupV3, currentGroup: PwGroupV3, groupIterator: Iterator) { + + if (currentGroup.parent == null && (previousGroup.level + 1) == currentGroup.level) { + // Current group has an increment level compare to the previous, current group is a child + previousGroup.addChildGroup(currentGroup) + currentGroup.parent = previousGroup + } else if (previousGroup.parent != null && previousGroup.level == currentGroup.level) { + // In the same level, previous parent is the same as previous group + previousGroup.parent!!.addChildGroup(currentGroup) + currentGroup.parent = previousGroup.parent + } else if (previousGroup.parent != null) { + // Previous group has a higher level than the current group, check it's parent + buildTreeGroups(previousGroup.parent!!, currentGroup, groupIterator) + } + + // Next current group + if (groupIterator.hasNext()){ + buildTreeGroups(currentGroup, groupIterator.next(), groupIterator) + } + } + + private fun constructTreeFromIndex() { + mDatabaseToOpen.rootGroup?.let { + + // add each group + val groupIterator = mDatabaseToOpen.getGroupIndexes().iterator() + if (groupIterator.hasNext()) + buildTreeGroups(it, groupIterator.next(), groupIterator) + + // add each child + for (currentEntry in mDatabaseToOpen.getEntryIndexes()) { + if (currentEntry.parent != null) { + // Only the parent id is known so complete the info + val parentGroupRetrieve = mDatabaseToOpen.getGroupById(currentEntry.parent!!.nodeId) + parentGroupRetrieve?.addChildEntry(currentEntry) + currentEntry.parent = parentGroupRetrieve + } + } + } + } + /** * Parse and save one record from binary file. * @param buf diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt index 00145ca13..3eb002310 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt @@ -533,7 +533,6 @@ class ImporterV4(private val streamDir: File) : Importer() { KdbContext.Entry -> if (name.equals(PwDatabaseV4XML.ElemUuid, ignoreCase = true)) { ctxEntry?.nodeId = PwNodeIdUUID(readUuid(xpp)) - ctxEntry?.let { mDatabase.addEntryIndex(it) } } else if (name.equals(PwDatabaseV4XML.ElemIcon, ignoreCase = true)) { ctxEntry?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, 0).toInt()) } else if (name.equals(PwDatabaseV4XML.ElemCustomIconID, ignoreCase = true)) { @@ -633,7 +632,7 @@ class ImporterV4(private val streamDir: File) : Importer() { KdbContext.EntryHistory -> if (name.equals(PwDatabaseV4XML.ElemEntry, ignoreCase = true)) { ctxEntry = PwEntryV4() - ctxEntry?.let { ctxHistoryBase?.addToHistory(it) } + ctxEntry?.let { ctxHistoryBase?.addEntryToHistory(it) } entryInHistory = true return switchContext(ctx, KdbContext.Entry, xpp) @@ -731,15 +730,18 @@ class ImporterV4(private val streamDir: File) : Importer() { return KdbContext.GroupCustomData } else if (ctx == KdbContext.Entry && name.equals(PwDatabaseV4XML.ElemEntry, ignoreCase = true)) { - if (ctxEntry != null && ctxEntry?.id == PwDatabase.UUID_ZERO) { + + if (ctxEntry?.id == PwDatabase.UUID_ZERO) ctxEntry?.nodeId = mDatabase.newEntryId() - mDatabase.addEntryIndex(ctxEntry!!) - } if (entryInHistory) { ctxEntry = ctxHistoryBase return KdbContext.EntryHistory } + else if (ctxEntry != null) { + // Add entry to the index only when close the XML element + mDatabase.addEntryIndex(ctxEntry!!) + } return KdbContext.Group } else if (ctx == KdbContext.EntryTimes && name.equals(PwDatabaseV4XML.ElemTimes, ignoreCase = true)) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index b760b8384..d07d01aa0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -40,6 +40,12 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.list_entries_show_username_default)) } + fun showNumberEntries(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.list_groups_show_number_entries_key), + context.resources.getBoolean(R.bool.list_groups_show_number_entries_default)) + } + /** * Retrieve the text size in SP, verify the integrity of the size stored in preference */ @@ -130,7 +136,7 @@ object PreferencesUtil { fun getListSort(context: Context): SortNodeEnum { val prefs = PreferenceManager.getDefaultSharedPreferences(context) prefs.getString(context.getString(R.string.sort_node_key), - SortNodeEnum.TITLE.name)?.let { + SortNodeEnum.DB.name)?.let { return SortNodeEnum.valueOf(it) } return SortNodeEnum.DB diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt index 0a77df967..d75e1ac1e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt @@ -21,6 +21,7 @@ package com.kunzisoft.keepass.tasks import android.app.Activity import android.content.Context +import android.os.Bundle import android.util.Log import android.widget.Toast @@ -88,5 +89,5 @@ abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable? /** * Class to manage result from ActionRunnable */ - data class Result(var isSuccess: Boolean = true, var message: String? = null) + data class Result(var isSuccess: Boolean = true, var message: String? = null, var data: Bundle? = null) } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt index f2d60843b..4b398f2fd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt @@ -65,6 +65,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context, attrs: Attri private val lastAccessDateView: TextView private val expiresDateView: TextView + private val uuidView: TextView + val isUserNamePresent: Boolean get() = userNameContainerView.visibility == View.VISIBLE @@ -97,6 +99,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context, attrs: Attri lastAccessDateView = findViewById(R.id.entry_accessed) expiresDateView = findViewById(R.id.entry_expires) + uuidView = findViewById(R.id.entry_UUID) + val attrColorAccent = intArrayOf(R.attr.colorAccent) val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent) colorAccent = taColorAccent.getColor(0, Color.BLACK) @@ -238,6 +242,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context, attrs: Attri expiresDateView.text = constString } + fun assignUUID(uuid: UUID) { + uuidView.text = uuid.toString() + } + override fun generateDefaultLayoutParams(): LayoutParams { return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } diff --git a/app/src/main/res/drawable-v21/background_icon.xml b/app/src/main/res/drawable-v21/background_icon.xml new file mode 100644 index 000000000..4bf4cf1ca --- /dev/null +++ b/app/src/main/res/drawable-v21/background_icon.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/background_image_button.xml b/app/src/main/res/drawable-v21/background_image_button.xml index d1fa72b32..ae42055a0 100644 --- a/app/src/main/res/drawable-v21/background_image_button.xml +++ b/app/src/main/res/drawable-v21/background_image_button.xml @@ -6,7 +6,7 @@ - + - diff --git a/app/src/main/res/drawable/background_icon.xml b/app/src/main/res/drawable/background_icon.xml new file mode 100644 index 000000000..a7b052b92 --- /dev/null +++ b/app/src/main/res/drawable/background_icon.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_image_button.xml b/app/src/main/res/drawable/background_image_button.xml index f816c317d..b7a501a66 100644 --- a/app/src/main/res/drawable/background_image_button.xml +++ b/app/src/main/res/drawable/background_image_button.xml @@ -3,7 +3,7 @@ - + - + - diff --git a/app/src/main/res/drawable/background_text_info.xml b/app/src/main/res/drawable/background_text_info.xml new file mode 100644 index 000000000..981df6281 --- /dev/null +++ b/app/src/main/res/drawable/background_text_info.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml index 66df5dea7..3ce92f730 100644 --- a/app/src/main/res/layout/activity_entry.xml +++ b/app/src/main/res/layout/activity_entry.xml @@ -52,12 +52,13 @@ android:gravity="center" android:paddingBottom="12dp" style="@style/KeepassDXStyle.TextAppearance.Default"> - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_file_selection.xml b/app/src/main/res/layout/activity_file_selection.xml index 2ae62c2ce..bd587abf0 100644 --- a/app/src/main/res/layout/activity_file_selection.xml +++ b/app/src/main/res/layout/activity_file_selection.xml @@ -197,21 +197,17 @@ + android:src="@drawable/ic_link_white_24dp" + android:contentDescription="@string/content_description_show_file_link"/> + android:background="?attr/colorPrimaryDark"> diff --git a/app/src/main/res/layout/activity_password.xml b/app/src/main/res/layout/activity_password.xml index 940ba1ed0..d353dcefe 100644 --- a/app/src/main/res/layout/activity_password.xml +++ b/app/src/main/res/layout/activity_password.xml @@ -53,6 +53,7 @@ android:id="@+id/default_database" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minHeight="48dp" android:layout_gravity="end" style="@style/KeepassDXStyle.TextAppearance.Small" android:textColor="?android:attr/textColorHintInverse" @@ -61,7 +62,6 @@ android:layout_marginStart="24dp" android:layout_marginRight="12dp" android:layout_marginEnd="12dp" - android:layout_marginBottom="6dp" android:text="@string/default_checkbox" /> @@ -138,6 +139,8 @@ android:layout_height="wrap_content" android:layout_toRightOf="@+id/password_checkbox" android:layout_toEndOf="@+id/password_checkbox" + android:importantForAccessibility="no" + android:importantForAutofill="no" app:passwordToggleEnabled="true" app:passwordToggleTint="?attr/colorAccent"> @@ -145,6 +148,7 @@ android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="48dp" android:hint="@string/password" android:inputType="textPassword" android:importantForAccessibility="no" @@ -165,6 +169,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="20dp" + android:contentDescription="@string/content_description_keyfile_checkbox" android:focusable="false" android:layout_alignBottom="@+id/input_entry_keyfile" android:gravity="center_vertical" /> @@ -173,6 +178,8 @@ android:id="@+id/input_entry_keyfile" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:importantForAccessibility="no" + android:importantForAutofill="no" android:layout_toEndOf="@+id/keyfile_checkox" android:layout_toRightOf="@+id/keyfile_checkox" android:layout_toLeftOf="@+id/browse_button" @@ -182,6 +189,7 @@ android:id="@+id/pass_keyfile" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="48dp" android:hint="@string/entry_keyfile" android:inputType="textUri" android:importantForAccessibility="no" @@ -192,12 +200,13 @@ diff --git a/app/src/main/res/layout/fragment_file_creation.xml b/app/src/main/res/layout/fragment_file_creation.xml index 6f68a916f..175ad8bdf 100644 --- a/app/src/main/res/layout/fragment_file_creation.xml +++ b/app/src/main/res/layout/fragment_file_creation.xml @@ -17,6 +17,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="6dp" + android:contentDescription="@string/content_description_open_file" android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_alignBottom="@+id/folder_path_input_layout" diff --git a/app/src/main/res/layout/fragment_generate_password.xml b/app/src/main/res/layout/fragment_generate_password.xml index 8507d219d..52847a44b 100644 --- a/app/src/main/res/layout/fragment_generate_password.xml +++ b/app/src/main/res/layout/fragment_generate_password.xml @@ -33,7 +33,8 @@ android:layout_height="wrap_content" android:orientation="vertical"> - -