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)
|
||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 (Thx @Dev-ClayP)
|
||||
* 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)
|
||||
* Fix merge deletion #1516
|
||||
* Fix space in search #175
|
||||
* Fix deletable recycle bin #2163
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.1.7)
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 35
|
||||
versionCode = 140
|
||||
versionName = "4.2.0beta01"
|
||||
versionCode = 142
|
||||
versionName = "4.2.0beta02"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -152,7 +152,7 @@ dependencies {
|
||||
|
||||
// Credentials Provider
|
||||
implementation "androidx.credentials:credentials:1.2.2"
|
||||
|
||||
|
||||
// Modules import
|
||||
implementation project(path: ':database')
|
||||
implementation project(path: ':icon-pack')
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.biometric.BiometricManager
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -108,7 +109,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||
|
||||
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)
|
||||
|
||||
@@ -176,7 +181,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel.checkConditionToStoreCredential(
|
||||
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
|
||||
condition = verified
|
||||
)
|
||||
}
|
||||
@@ -241,29 +246,31 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// New value received
|
||||
uiState.credentialRequiredCipher?.let { cipher ->
|
||||
mDeviceUnlockViewModel.encryptCredential(
|
||||
credential = getCredentialForEncryption(),
|
||||
cipher = cipher
|
||||
)
|
||||
}
|
||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||
onCredentialEncrypted(cipherEncryptDatabase)
|
||||
mDeviceUnlockViewModel.consumeCredentialEncrypted()
|
||||
}
|
||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||
onCredentialDecrypted(cipherDecryptDatabase)
|
||||
mDeviceUnlockViewModel.consumeCredentialDecrypted()
|
||||
}
|
||||
uiState.exception?.let { error ->
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
deviceUnlockError(error, this@MainCredentialActivity),
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
mDeviceUnlockViewModel.exceptionShown()
|
||||
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
|
||||
deviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// New value received
|
||||
uiState.credentialRequiredCipher?.let { cipher ->
|
||||
deviceUnlockViewModel.encryptCredential(
|
||||
credential = getCredentialForEncryption(),
|
||||
cipher = cipher
|
||||
)
|
||||
}
|
||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||
onCredentialEncrypted(cipherEncryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialEncrypted()
|
||||
}
|
||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||
onCredentialDecrypted(cipherDecryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialDecrypted()
|
||||
}
|
||||
uiState.exception?.let { error ->
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
deviceUnlockError(error, this@MainCredentialActivity),
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
deviceUnlockViewModel.exceptionShown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,7 +523,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel.connect(databaseFileUri)
|
||||
mDeviceUnlockViewModel?.connect(databaseFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,7 +667,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
try {
|
||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to find read mode menu")
|
||||
Log.e(TAG, "Unable to find read mode menu", e)
|
||||
}
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
@@ -689,7 +696,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (ignored: Exception) {}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +733,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel.disconnect()
|
||||
mDeviceUnlockViewModel?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
|
||||
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,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean): SortDialogFragment {
|
||||
recycleBinBottom: Boolean?): SortDialogFragment {
|
||||
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()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
|
||||
@@ -76,9 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
|
||||
private var mRecycleBinEnable: Boolean = false
|
||||
private var mRecycleBin: Group? = null
|
||||
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
@@ -102,21 +99,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (mRecycleBinEnable) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
if (mDatabase?.isRecycleBinEnabled == true) {
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
} else {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context)
|
||||
)
|
||||
}
|
||||
|
||||
} else null
|
||||
)
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
}
|
||||
true
|
||||
@@ -165,9 +155,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
||||
mRecycleBin = database?.recycleBin
|
||||
|
||||
context?.let { context ->
|
||||
database?.let { database ->
|
||||
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,
|
||||
nodes: List<Node>,
|
||||
menuListener: NodesActionMenuListener?,
|
||||
@@ -336,8 +328,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (database.isReadOnly
|
||||
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
@@ -357,8 +348,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (database.isReadOnly
|
||||
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
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.CustomData
|
||||
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.DatabaseKDBX
|
||||
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.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.utils.readAllBytes
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
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
|
||||
* To make a merge as accurate as possible
|
||||
*/
|
||||
@@ -302,32 +304,113 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
}
|
||||
|
||||
// Manage deleted objects
|
||||
databaseToMerge.deletedObjects.forEach { deletedObject ->
|
||||
val deletedObjectId = deletedObject.uuid
|
||||
val databaseEntry = database.getEntryById(deletedObjectId)
|
||||
val databaseGroup = database.getGroupById(deletedObjectId)
|
||||
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
|
||||
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)
|
||||
}
|
||||
val deletedObjects = databaseToMerge.deletedObjects
|
||||
deletedObjects.forEach { deletedObject ->
|
||||
deleteEntry(deletedObject)
|
||||
deleteGroup(deletedObject, deletedObjects)
|
||||
deleteIcon(deletedObject)
|
||||
// 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]
|
||||
*/
|
||||
|
||||
@@ -154,7 +154,7 @@ class SearchHelper {
|
||||
if (searchParameters.searchByDomain) {
|
||||
try {
|
||||
stringToCheck.inTheSameDomainAs(word, sameSubDomain = true)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
} else null
|
||||
@@ -220,10 +220,18 @@ class SearchHelper {
|
||||
regex.matches(stringToCheck)
|
||||
} else {
|
||||
specialComparison?.invoke(stringToCheck, searchParameters.searchQuery)
|
||||
?: stringToCheck.contains(
|
||||
searchParameters.searchQuery,
|
||||
!searchParameters.caseSensitive
|
||||
)
|
||||
?: run {
|
||||
// Search with space separator #175
|
||||
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> {
|
||||
val size = readInt()
|
||||
val map = HashMap<K, V>(size)
|
||||
for (i in 0 until size) {
|
||||
(0 until size).forEach { i ->
|
||||
val key: K? = try {
|
||||
when {
|
||||
SDK_INT >= 33 -> readParcelable(K::class.java.classLoader, K::class.java)
|
||||
else -> @Suppress("DEPRECATION") readParcelable(K::class.java.classLoader)
|
||||
}
|
||||
} catch (e: Exception) { null }
|
||||
} catch (_: Exception) { null }
|
||||
val value: V? = try {
|
||||
when {
|
||||
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
|
||||
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
|
||||
}
|
||||
} catch (e: Exception) { null }
|
||||
} catch (_: Exception) { null }
|
||||
if (key != null && value != null)
|
||||
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> {
|
||||
val size = readInt()
|
||||
val map = LinkedHashMap<String, V>(size)
|
||||
for (i in 0 until size) {
|
||||
(0 until size).forEach { i ->
|
||||
val key: String? = readString()
|
||||
val value: V? = try {
|
||||
when {
|
||||
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
|
||||
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
|
||||
}
|
||||
} catch (e: Exception) { null }
|
||||
} catch (_: Exception) { null }
|
||||
if (key != null && value != null)
|
||||
map[key] = value
|
||||
}
|
||||
@@ -178,7 +178,7 @@ fun Parcel.writeStringIntMap(map: LinkedHashMap<String, Int>) {
|
||||
fun Parcel.readStringIntMap(): LinkedHashMap<String, Int> {
|
||||
val size = readInt()
|
||||
val map = LinkedHashMap<String, Int>(size)
|
||||
for (i in 0 until size) {
|
||||
(0 until size).forEach { i ->
|
||||
val key: String? = readString()
|
||||
val value: Int = readInt()
|
||||
if (key != null)
|
||||
@@ -200,7 +200,7 @@ fun Parcel.writeStringStringMap(map: MutableMap<String, String>) {
|
||||
fun Parcel.readStringStringMap(): LinkedHashMap<String, String> {
|
||||
val size = readInt()
|
||||
val map = LinkedHashMap<String, String>(size)
|
||||
for (i in 0 until size) {
|
||||
(0 until size).forEach { i ->
|
||||
val key: String? = readString()
|
||||
val value: String? = readString()
|
||||
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