diff --git a/CHANGELOG b/CHANGELOG index fbf1229e7..4a38fa69c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +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 diff --git a/app/build.gradle b/app/build.gradle index f1eed1aeb..b0b10d4f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 14 targetSdkVersion 27 - versionCode = 20 - versionName = "2.5.0.0beta20" + versionCode = 21 + versionName = "2.5.0.0beta21" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" 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..a30611c41 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") } 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..9f6c58cdc 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,12 @@ 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 override fun onAttach(context: Context?) { super.onAttach(context) @@ -50,18 +50,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)) 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..237987f7a 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,7 +82,7 @@ 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 { @@ -121,13 +123,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 +141,12 @@ 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() } - } /** @@ -239,6 +242,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().size.toString() + textSize = infoTextSize + visibility = View.VISIBLE + } + } else { + holder.numberChildren?.visibility = View.GONE + } + } } override fun getItemCount(): Int { @@ -356,6 +370,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..c0a293b60 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, recycleBinBottom) + 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/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..22c179e43 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 @@ -195,12 +195,15 @@ class GroupVersioned : NodeVersioned, PwGroupInterface { + fun getChildren(withoutMetaStream: Boolean = true): List { val children = ArrayList() children.addAll(getChildGroups()) pwGroupV3?.let { - children.addAll(getChildEntries().filter { !it.isMetaStream }) + if (withoutMetaStream) + children.addAll(getChildEntries().filter { !it.isMetaStream }) + else + children.addAll(getChildEntries()) } pwGroupV4?.let { // No MetasStream in V4 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..44a3746dc 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(false)?.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/PwDatabaseV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.kt index cd56208c7..7a042a13d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.kt @@ -27,7 +27,6 @@ import java.io.InputStream import java.security.DigestOutputStream import java.security.MessageDigest import java.security.NoSuchAlgorithmException -import java.util.* /** * @author Naomaru Itoi @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 * 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..cf557aae9 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 < @@ -13,9 +12,9 @@ abstract class PwGroup private var titleGroup = "" @Transient - private val childGroups = ArrayList() + private val childGroups = LinkedHashSet() @Transient - private val childEntries = ArrayList() + private val childEntries = LinkedHashSet() constructor() : super() @@ -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 @@ -38,18 +39,20 @@ abstract class PwGroup set(value) { titleGroup = value } override fun getChildGroups(): MutableList { - return childGroups + return childGroups.toMutableList() } override fun getChildEntries(): MutableList { - return childEntries + return childEntries.toMutableList() } override fun addChildGroup(group: Group) { + // TODO duplicate UUID this.childGroups.add(group) } override fun addChildEntry(entry: Entry) { + // TODO duplicate UUID 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/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_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_edit.xml b/app/src/main/res/layout/activity_entry_edit.xml index 68e49560b..365f0abd6 100644 --- a/app/src/main/res/layout/activity_entry_edit.xml +++ b/app/src/main/res/layout/activity_entry_edit.xml @@ -65,6 +65,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" android:src="@drawable/ic_save_white_24dp" + android:contentDescription="@string/content_description_entry_save" app:useCompatPadding="true" style="@style/KeepassDXStyle.Fab"/> \ 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..1d5b47f84 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" /> @@ -165,6 +166,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" /> @@ -192,12 +194,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"> - -