mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'develop' into release/4.2.0
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
KeePassDX(4.2.0)
|
KeePassDX(4.2.0)
|
||||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 (Thx @Dev-ClayP)
|
|
||||||
* Passkeys management #1421 #2097 (Thx @cali-95)
|
* Passkeys management #1421 #2097 (Thx @cali-95)
|
||||||
|
|
||||||
|
KeePassDX(4.1.8)
|
||||||
|
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||||
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
|
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
|
||||||
|
* Fix merge deletion #1516
|
||||||
|
* Fix space in search #175
|
||||||
|
* Fix deletable recycle bin #2163
|
||||||
* Small fixes
|
* Small fixes
|
||||||
|
|
||||||
KeePassDX(4.1.7)
|
KeePassDX(4.1.7)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 35
|
targetSdkVersion 35
|
||||||
versionCode = 140
|
versionCode = 142
|
||||||
versionName = "4.2.0beta01"
|
versionName = "4.2.0beta02"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -152,7 +152,7 @@ dependencies {
|
|||||||
|
|
||||||
// Credentials Provider
|
// Credentials Provider
|
||||||
implementation "androidx.credentials:credentials:1.2.2"
|
implementation "androidx.credentials:credentials:1.2.2"
|
||||||
|
|
||||||
// Modules import
|
// Modules import
|
||||||
implementation project(path: ':database')
|
implementation project(path: ':database')
|
||||||
implementation project(path: ':icon-pack')
|
implementation project(path: ':icon-pack')
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import androidx.biometric.BiometricManager
|
|||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -108,7 +109,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||||
|
|
||||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by viewModels()
|
private val mDeviceUnlockViewModel: DeviceUnlockViewModel? by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
ViewModelProvider(this)[DeviceUnlockViewModel::class.java]
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
@@ -176,7 +181,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.checkConditionToStoreCredential(
|
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
|
||||||
condition = verified
|
condition = verified
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -241,29 +246,31 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
|
||||||
// New value received
|
deviceUnlockViewModel.uiState.collect { uiState ->
|
||||||
uiState.credentialRequiredCipher?.let { cipher ->
|
// New value received
|
||||||
mDeviceUnlockViewModel.encryptCredential(
|
uiState.credentialRequiredCipher?.let { cipher ->
|
||||||
credential = getCredentialForEncryption(),
|
deviceUnlockViewModel.encryptCredential(
|
||||||
cipher = cipher
|
credential = getCredentialForEncryption(),
|
||||||
)
|
cipher = cipher
|
||||||
}
|
)
|
||||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
}
|
||||||
onCredentialEncrypted(cipherEncryptDatabase)
|
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||||
mDeviceUnlockViewModel.consumeCredentialEncrypted()
|
onCredentialEncrypted(cipherEncryptDatabase)
|
||||||
}
|
deviceUnlockViewModel.consumeCredentialEncrypted()
|
||||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
}
|
||||||
onCredentialDecrypted(cipherDecryptDatabase)
|
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||||
mDeviceUnlockViewModel.consumeCredentialDecrypted()
|
onCredentialDecrypted(cipherDecryptDatabase)
|
||||||
}
|
deviceUnlockViewModel.consumeCredentialDecrypted()
|
||||||
uiState.exception?.let { error ->
|
}
|
||||||
Snackbar.make(
|
uiState.exception?.let { error ->
|
||||||
coordinatorLayout,
|
Snackbar.make(
|
||||||
deviceUnlockError(error, this@MainCredentialActivity),
|
coordinatorLayout,
|
||||||
Snackbar.LENGTH_LONG
|
deviceUnlockError(error, this@MainCredentialActivity),
|
||||||
).asError().show()
|
Snackbar.LENGTH_LONG
|
||||||
mDeviceUnlockViewModel.exceptionShown()
|
).asError().show()
|
||||||
|
deviceUnlockViewModel.exceptionShown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,7 +523,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.connect(databaseFileUri)
|
mDeviceUnlockViewModel?.connect(databaseFileUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,7 +667,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
try {
|
try {
|
||||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to find read mode menu")
|
Log.e(TAG, "Unable to find read mode menu", e)
|
||||||
}
|
}
|
||||||
performedNextEducation(menu)
|
performedNextEducation(menu)
|
||||||
},
|
},
|
||||||
@@ -689,7 +696,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ignored: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +733,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.disconnect()
|
mDeviceUnlockViewModel?.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
|
|||||||
return bundle
|
return bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
|
||||||
ascending: Boolean,
|
|
||||||
groupsBefore: Boolean): SortDialogFragment {
|
|
||||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
|
||||||
val fragment = SortDialogFragment()
|
|
||||||
fragment.arguments = bundle
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||||
ascending: Boolean,
|
ascending: Boolean,
|
||||||
groupsBefore: Boolean,
|
groupsBefore: Boolean,
|
||||||
recycleBinBottom: Boolean): SortDialogFragment {
|
recycleBinBottom: Boolean?): SortDialogFragment {
|
||||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
recycleBinBottom?.let {
|
||||||
|
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||||
|
}
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -76,9 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
|
|
||||||
private var mRecycleBinEnable: Boolean = false
|
|
||||||
private var mRecycleBin: Group? = null
|
|
||||||
|
|
||||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
@@ -102,21 +99,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (mRecycleBinEnable) {
|
SortDialogFragment.getInstance(
|
||||||
SortDialogFragment.getInstance(
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getGroupsBeforeSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context),
|
if (mDatabase?.isRecycleBinEnabled == true) {
|
||||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
)
|
} else null
|
||||||
} else {
|
)
|
||||||
SortDialogFragment.getInstance(
|
|
||||||
PreferencesUtil.getListSort(context),
|
|
||||||
PreferencesUtil.getAscendingSort(context),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -165,9 +155,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
|
||||||
mRecycleBin = database?.recycleBin
|
|
||||||
|
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
mAdapter = NodesAdapter(context, database).apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
@@ -312,6 +299,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun containsRecycleBin(nodes: List<Node>): Boolean {
|
||||||
|
return mDatabase?.isRecycleBinEnabled == true
|
||||||
|
&& nodes.any { it == mDatabase?.recycleBin }
|
||||||
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(database: ContextualDatabase,
|
fun actionNodesCallback(database: ContextualDatabase,
|
||||||
nodes: List<Node>,
|
nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?,
|
menuListener: NodesActionMenuListener?,
|
||||||
@@ -336,8 +328,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
// Open and Edit for a single item
|
// Open and Edit for a single item
|
||||||
if (nodes.size == 1) {
|
if (nodes.size == 1) {
|
||||||
// Edition
|
// Edition
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||||
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -357,8 +348,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||||
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.merge
|
|||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.CustomData
|
import com.kunzisoft.keepass.database.element.CustomData
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
@@ -32,9 +33,10 @@ import com.kunzisoft.keepass.database.element.node.NodeHandler
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.utils.readAllBytes
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||||
|
|
||||||
@@ -180,7 +182,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge a KDB> database in a KDBX database,
|
* Merge a KDBX database in a KDBX database,
|
||||||
* Try to take into account the modification date of each element
|
* Try to take into account the modification date of each element
|
||||||
* To make a merge as accurate as possible
|
* To make a merge as accurate as possible
|
||||||
*/
|
*/
|
||||||
@@ -302,32 +304,113 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Manage deleted objects
|
// Manage deleted objects
|
||||||
databaseToMerge.deletedObjects.forEach { deletedObject ->
|
val deletedObjects = databaseToMerge.deletedObjects
|
||||||
val deletedObjectId = deletedObject.uuid
|
deletedObjects.forEach { deletedObject ->
|
||||||
val databaseEntry = database.getEntryById(deletedObjectId)
|
deleteEntry(deletedObject)
|
||||||
val databaseGroup = database.getGroupById(deletedObjectId)
|
deleteGroup(deletedObject, deletedObjects)
|
||||||
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
|
deleteIcon(deletedObject)
|
||||||
val databaseIconModificationTime = databaseIcon?.lastModificationTime
|
|
||||||
if (databaseEntry != null
|
|
||||||
&& deletedObject.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
|
|
||||||
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
|
|
||||||
}
|
|
||||||
if (databaseGroup != null
|
|
||||||
&& deletedObject.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
|
|
||||||
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
|
|
||||||
}
|
|
||||||
if (databaseIcon != null
|
|
||||||
&& (
|
|
||||||
databaseIconModificationTime == null
|
|
||||||
|| (deletedObject.deletionTime.isAfter(databaseIconModificationTime))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
database.removeCustomIcon(deletedObjectId)
|
|
||||||
}
|
|
||||||
// Attachments are removed and optimized during the database save
|
// Attachments are removed and optimized during the database save
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an entry from the database with the [deletedEntry] id
|
||||||
|
*/
|
||||||
|
private fun deleteEntry(deletedEntry: DeletedObject) {
|
||||||
|
val databaseEntry = database.getEntryById(deletedEntry.uuid)
|
||||||
|
if (databaseEntry != null
|
||||||
|
&& deletedEntry.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
|
||||||
|
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a node is in the list of deleted objects
|
||||||
|
*/
|
||||||
|
private fun Set<DeletedObject>.containsNode(node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>): Boolean {
|
||||||
|
return this.any { it.uuid == node.nodeId.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a node is not in the list of deleted objects
|
||||||
|
*/
|
||||||
|
private fun Set<DeletedObject>.notContainsNode(node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>): Boolean {
|
||||||
|
return !this.containsNode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first parent not deleted
|
||||||
|
*/
|
||||||
|
private fun firstNotDeletedParent(
|
||||||
|
node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>,
|
||||||
|
deletedObjects: Set<DeletedObject>
|
||||||
|
): GroupKDBX? {
|
||||||
|
var parent = node.parent
|
||||||
|
while (parent != null && deletedObjects.containsNode(parent)) {
|
||||||
|
parent = node.parent
|
||||||
|
}
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a group from the database with the [deletedGroup] id
|
||||||
|
* Recursively check whether a group to be deleted contains a node not to be deleted with [deletedObjects]
|
||||||
|
* and move it to the first parent that has not been deleted.
|
||||||
|
*/
|
||||||
|
private fun deleteGroup(deletedGroup: DeletedObject, deletedObjects: Set<DeletedObject>) {
|
||||||
|
val databaseGroup = database.getGroupById(deletedGroup.uuid)
|
||||||
|
if (databaseGroup != null
|
||||||
|
&& deletedGroup.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
|
||||||
|
// Must be in dedicated list to prevent modification collision
|
||||||
|
val entriesToMove = mutableListOf<EntryKDBX>()
|
||||||
|
databaseGroup.getChildEntries().forEach { child ->
|
||||||
|
// If the child entry is not a deleted object,
|
||||||
|
if (deletedObjects.notContainsNode(child)) {
|
||||||
|
entriesToMove.add(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val groupsToMove = mutableListOf<GroupKDBX>()
|
||||||
|
databaseGroup.getChildGroups().forEach { child ->
|
||||||
|
// Move the group to the first parent not deleted
|
||||||
|
// the deleted objects will take care of remove it later
|
||||||
|
groupsToMove.add(child)
|
||||||
|
}
|
||||||
|
// For each node to move, move it
|
||||||
|
// try to move the child entry in the first parent not deleted
|
||||||
|
entriesToMove.forEach { child ->
|
||||||
|
database.removeEntryFrom(child, child.parent)
|
||||||
|
database.addEntryTo(
|
||||||
|
child,
|
||||||
|
firstNotDeletedParent(databaseGroup, deletedObjects)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
groupsToMove.forEach { child ->
|
||||||
|
database.removeGroupFrom(child, child.parent)
|
||||||
|
database.addGroupTo(
|
||||||
|
child,
|
||||||
|
firstNotDeletedParent(databaseGroup, deletedObjects)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Then delete the group
|
||||||
|
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an icon from the database with the [deletedIcon] id
|
||||||
|
*/
|
||||||
|
private fun deleteIcon(deletedIcon: DeletedObject) {
|
||||||
|
val deletedObjectId = deletedIcon.uuid
|
||||||
|
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
|
||||||
|
val databaseIconModificationTime = databaseIcon?.lastModificationTime
|
||||||
|
if (databaseIcon != null
|
||||||
|
&& (databaseIconModificationTime == null
|
||||||
|
|| (deletedIcon.deletionTime.isAfter(databaseIconModificationTime)))
|
||||||
|
) {
|
||||||
|
database.removeCustomIcon(deletedObjectId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge [customDataToMerge] in [customData]
|
* Merge [customDataToMerge] in [customData]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class SearchHelper {
|
|||||||
if (searchParameters.searchByDomain) {
|
if (searchParameters.searchByDomain) {
|
||||||
try {
|
try {
|
||||||
stringToCheck.inTheSameDomainAs(word, sameSubDomain = true)
|
stringToCheck.inTheSameDomainAs(word, sameSubDomain = true)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
@@ -220,10 +220,18 @@ class SearchHelper {
|
|||||||
regex.matches(stringToCheck)
|
regex.matches(stringToCheck)
|
||||||
} else {
|
} else {
|
||||||
specialComparison?.invoke(stringToCheck, searchParameters.searchQuery)
|
specialComparison?.invoke(stringToCheck, searchParameters.searchQuery)
|
||||||
?: stringToCheck.contains(
|
?: run {
|
||||||
searchParameters.searchQuery,
|
// Search with space separator #175
|
||||||
!searchParameters.caseSensitive
|
var searchFound = true
|
||||||
)
|
searchParameters.searchQuery.split(" ").forEach { word ->
|
||||||
|
searchFound = searchFound
|
||||||
|
&& stringToCheck.contains(
|
||||||
|
word,
|
||||||
|
!searchParameters.caseSensitive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
searchFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,19 +119,19 @@ fun <K : Parcelable, V : Parcelable> Parcel.writeParcelableMap(map: Map<K, V>, f
|
|||||||
inline fun <reified K : Parcelable, reified V : Parcelable> Parcel.readParcelableMap(): Map<K, V> {
|
inline fun <reified K : Parcelable, reified V : Parcelable> Parcel.readParcelableMap(): Map<K, V> {
|
||||||
val size = readInt()
|
val size = readInt()
|
||||||
val map = HashMap<K, V>(size)
|
val map = HashMap<K, V>(size)
|
||||||
for (i in 0 until size) {
|
(0 until size).forEach { i ->
|
||||||
val key: K? = try {
|
val key: K? = try {
|
||||||
when {
|
when {
|
||||||
SDK_INT >= 33 -> readParcelable(K::class.java.classLoader, K::class.java)
|
SDK_INT >= 33 -> readParcelable(K::class.java.classLoader, K::class.java)
|
||||||
else -> @Suppress("DEPRECATION") readParcelable(K::class.java.classLoader)
|
else -> @Suppress("DEPRECATION") readParcelable(K::class.java.classLoader)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
val value: V? = try {
|
val value: V? = try {
|
||||||
when {
|
when {
|
||||||
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
|
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
|
||||||
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
|
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
if (key != null && value != null)
|
if (key != null && value != null)
|
||||||
map[key] = value
|
map[key] = value
|
||||||
}
|
}
|
||||||
@@ -151,14 +151,14 @@ fun <V : Parcelable> Parcel.writeStringParcelableMap(map: HashMap<String, V>, fl
|
|||||||
inline fun <reified V : Parcelable> Parcel.readStringParcelableMap(): LinkedHashMap<String, V> {
|
inline fun <reified V : Parcelable> Parcel.readStringParcelableMap(): LinkedHashMap<String, V> {
|
||||||
val size = readInt()
|
val size = readInt()
|
||||||
val map = LinkedHashMap<String, V>(size)
|
val map = LinkedHashMap<String, V>(size)
|
||||||
for (i in 0 until size) {
|
(0 until size).forEach { i ->
|
||||||
val key: String? = readString()
|
val key: String? = readString()
|
||||||
val value: V? = try {
|
val value: V? = try {
|
||||||
when {
|
when {
|
||||||
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
|
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
|
||||||
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
|
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
if (key != null && value != null)
|
if (key != null && value != null)
|
||||||
map[key] = value
|
map[key] = value
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ fun Parcel.writeStringIntMap(map: LinkedHashMap<String, Int>) {
|
|||||||
fun Parcel.readStringIntMap(): LinkedHashMap<String, Int> {
|
fun Parcel.readStringIntMap(): LinkedHashMap<String, Int> {
|
||||||
val size = readInt()
|
val size = readInt()
|
||||||
val map = LinkedHashMap<String, Int>(size)
|
val map = LinkedHashMap<String, Int>(size)
|
||||||
for (i in 0 until size) {
|
(0 until size).forEach { i ->
|
||||||
val key: String? = readString()
|
val key: String? = readString()
|
||||||
val value: Int = readInt()
|
val value: Int = readInt()
|
||||||
if (key != null)
|
if (key != null)
|
||||||
@@ -200,7 +200,7 @@ fun Parcel.writeStringStringMap(map: MutableMap<String, String>) {
|
|||||||
fun Parcel.readStringStringMap(): LinkedHashMap<String, String> {
|
fun Parcel.readStringStringMap(): LinkedHashMap<String, String> {
|
||||||
val size = readInt()
|
val size = readInt()
|
||||||
val map = LinkedHashMap<String, String>(size)
|
val map = LinkedHashMap<String, String>(size)
|
||||||
for (i in 0 until size) {
|
(0 until size).forEach { i ->
|
||||||
val key: String? = readString()
|
val key: String? = readString()
|
||||||
val value: String? = readString()
|
val value: String? = readString()
|
||||||
if (key != null && value != null)
|
if (key != null && value != null)
|
||||||
|
|||||||
6
fastlane/metadata/android/en-US/changelogs/141.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/141.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||||
|
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
|
||||||
|
* Fix merge deletion #1516
|
||||||
|
* Fix space in search #175
|
||||||
|
* Fix deletable recycle bin #2163
|
||||||
|
* Small fixes
|
||||||
2
fastlane/metadata/android/en-US/changelogs/142.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/142.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
* Passkeys management #1421 #2097 (Thx @cali-95)
|
||||||
|
* Small fixes
|
||||||
6
fastlane/metadata/android/fr-FR/changelogs/141.txt
Normal file
6
fastlane/metadata/android/fr-FR/changelogs/141.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
* Mise à jour vers API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||||
|
* Sauvegarde du dernier état lecture seule #2099 #2100 (Thx @rmacklin)
|
||||||
|
* Correction de la suppression lors d'un merge #1516
|
||||||
|
* Correction des espaces dans la recherche #175
|
||||||
|
* Correction de la poubelle supprimable #2163
|
||||||
|
* Petites corrections
|
||||||
2
fastlane/metadata/android/fr-FR/changelogs/142.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/142.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
* Gestion des Passkeys #1421 #2097 (Thx @cali-95)
|
||||||
|
* Petites corrections
|
||||||
Reference in New Issue
Block a user