Merge branch 'release/4.2.3'

This commit is contained in:
J-Jamet
2025-10-29 18:37:19 +01:00
14 changed files with 205 additions and 95 deletions

View File

@@ -1,3 +1,9 @@
KeePassDX(4.2.3)
* Fix multiple Passkey selection #2253
* Fix database dialog subtitle #2254
* Fix save search info if URL present #2255
* Small fixes
KeePassDX(4.2.2) KeePassDX(4.2.2)
* Fix database merge algorithm #2223 * Fix database merge algorithm #2223
* Fix save search info #2243 * Fix save search info #2243

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 35 targetSdkVersion 35
versionCode = 147 versionCode = 148
versionName = "4.2.2" versionName = "4.2.3"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -841,7 +841,7 @@ class GroupActivity : DatabaseLockActivity(),
// Open child group // Open child group
loadMainGroup(GroupState(group.nodeId, 0)) loadMainGroup(GroupState(group.nodeId, 0))
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Group") Log.e(TAG, "Node can't be cast in Group", e)
} }
Type.ENTRY -> try { Type.ENTRY -> try {
@@ -867,6 +867,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!database.isReadOnly if (!database.isReadOnly
&& searchInfo != null && searchInfo != null
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity) && PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
&& entryVersioned.containsSearchInfo(database, searchInfo).not()
) { ) {
updateEntryWithRegisterInfo( updateEntryWithRegisterInfo(
database, database,
@@ -884,6 +885,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!database.isReadOnly if (!database.isReadOnly
&& searchInfo != null && searchInfo != null
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity) && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
&& entryVersioned.containsSearchInfo(database, searchInfo).not()
) { ) {
updateEntryWithRegisterInfo( updateEntryWithRegisterInfo(
database, database,
@@ -912,7 +914,7 @@ class GroupActivity : DatabaseLockActivity(),
finish() finish()
}) })
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Entry") Log.e(TAG, "Node can't be cast in Entry", e)
} }
} }
} }
@@ -981,6 +983,17 @@ class GroupActivity : DatabaseLockActivity(),
updateEntry(entry, newEntry) updateEntry(entry, newEntry)
} }
private fun Entry.containsSearchInfo(
database: ContextualDatabase,
searchInfo: SearchInfo
): Boolean {
return getEntryInfo(
database,
raw = true,
removeTemplateConfiguration = false
).containsSearchInfo(searchInfo)
}
private fun finishNodeAction() { private fun finishNodeAction() {
actionNodeMode?.finish() actionNodeMode?.finish()
} }

View File

@@ -1,13 +1,17 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.Manifest import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -16,6 +20,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
@@ -54,13 +59,67 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
} }
} }
/**
* Useful to only waiting for the activity result and prevent any parallel action
*/
var credentialResultLaunched = false
/**
* Utility activity result launcher,
* Used recursively, close each activity with return data
*/
protected var mCredentialActivityResultLauncher: CredentialActivityResultLauncher =
CredentialActivityResultLauncher(
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase = false,
resultCode = it.resultCode,
data = it.data
)
}
)
/**
* Custom ActivityResultLauncher to manage the database action
*/
protected inner class CredentialActivityResultLauncher(
val builder: ActivityResultLauncher<Intent>
) : ActivityResultLauncher<Intent>() {
override fun launch(
input: Intent?,
options: ActivityOptionsCompat?
) {
credentialResultLaunched = true
builder.launch(input, options)
}
override fun unregister() {
builder.unregister()
}
override fun getContract(): ActivityResultContract<Intent?, *> {
return builder.getContract()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
) {
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
}
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState -> mDatabaseViewModel.actionState.collect { uiState ->
if (credentialResultLaunched.not()) {
when (uiState) { when (uiState) {
is DatabaseViewModel.ActionState.Loading -> {} is DatabaseViewModel.ActionState.Wait -> {}
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> { is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
if (finishActivityIfReloadRequested()) { if (finishActivityIfReloadRequested()) {
finish() finish()
@@ -82,13 +141,13 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
) )
} }
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> { is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
progressTaskViewModel.start(uiState.progressMessage) progressTaskViewModel.show(uiState.progressMessage)
} }
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> { is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
progressTaskViewModel.update(uiState.progressMessage) progressTaskViewModel.show(uiState.progressMessage)
} }
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> { is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
progressTaskViewModel.stop() progressTaskViewModel.hide()
} }
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> { is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished( onDatabaseActionFinished(
@@ -96,7 +155,8 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
uiState.actionTask, uiState.actionTask,
uiState.result uiState.result
) )
progressTaskViewModel.stop() progressTaskViewModel.hide()
}
} }
} }
} }
@@ -106,9 +166,9 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.RESUMED) {
progressTaskViewModel.progressTaskState.collect { state -> progressTaskViewModel.progressTaskState.collect { state ->
when (state) { when (state) {
ProgressTaskViewModel.ProgressTaskState.Start -> is ProgressTaskViewModel.ProgressTaskState.Show ->
showDialog() startDialog()
ProgressTaskViewModel.ProgressTaskState.Stop -> is ProgressTaskViewModel.ProgressTaskState.Hide ->
stopDialog() stopDialog()
} }
} }
@@ -117,6 +177,7 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database -> mDatabaseViewModel.databaseState.collect { database ->
if (credentialResultLaunched.not()) {
// Nullable function // Nullable function
onUnknownDatabaseRetrieved(database) onUnknownDatabaseRetrieved(database)
database?.let { database?.let {
@@ -126,6 +187,12 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
} }
} }
} }
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY, credentialResultLaunched)
super.onSaveInstanceState(outState)
}
/** /**
* Nullable function to retrieve a database * Nullable function to retrieve a database
@@ -207,7 +274,7 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
} }
} }
private fun showDialog() { private fun startDialog() {
lifecycleScope.launch { lifecycleScope.launch {
if (showDatabaseDialog()) { if (showDatabaseDialog()) {
if (progressTaskDialogFragment == null) { if (progressTaskDialogFragment == null) {
@@ -233,4 +300,8 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected open fun showDatabaseDialog(): Boolean { protected open fun showDatabaseDialog(): Boolean {
return true return true
} }
companion object {
const val CREDENTIAL_RESULT_LAUNCHER_KEY = "com.kunzisoft.keepass.CREDENTIAL_RESULT_LAUNCHER_KEY"
}
} }

View File

@@ -1,12 +1,9 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
@@ -15,7 +12,6 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveReg
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
@@ -34,21 +30,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
private var mToolbarSpecial: ToolbarSpecial? = null private var mToolbarSpecial: ToolbarSpecial? = null
/**
* Utility activity result launcher,
* Used recursively, close each activity with return data
*/
protected open var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase = false,
resultCode = it.resultCode,
data = it.data
)
}
open fun onDatabaseBackPressed() { open fun onDatabaseBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT) if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode() onCancelSpecialMode()

View File

@@ -78,6 +78,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
context = this@EntrySelectionLauncherActivity, context = this@EntrySelectionLauncherActivity,
searchInfo = uiState.searchInfo searchInfo = uiState.searchInfo
) )
finish()
} }
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> { is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
GroupActivity.launchForSearch( GroupActivity.launchForSearch(
@@ -85,6 +86,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
database = uiState.database, database = uiState.database,
searchInfo = uiState.searchInfo searchInfo = uiState.searchInfo
) )
finish()
} }
} }
} }

View File

@@ -661,7 +661,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
private fun updateMessage(resId: Int) { private fun updateMessage(resId: Int) {
mProgressMessage.messageId = resId mProgressMessage = mProgressMessage.copy(
messageId = resId
)
notifyProgressMessage() notifyProgressMessage()
} }

View File

@@ -70,16 +70,35 @@ open class ProgressTaskDialogFragment : DialogFragment() {
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
progressTaskViewModel.progressMessageState.collect { state -> progressTaskViewModel.progressTaskState.collect { state ->
updateView(titleView, when (state) {
state.titleId?.let { title -> getString(title) }) is ProgressTaskViewModel.ProgressTaskState.Show -> {
updateView(messageView, val value = state.value
state.messageId?.let { message -> getString(message) }) updateView(
updateView(warningView, titleView,
state.warningId?.let { warning -> getString(warning) }) value.titleId?.let { title ->
cancelButton?.isVisible = state.cancelable != null getString(title)
cancelButton?.setOnClickListener { })
state.cancelable?.invoke() updateView(
messageView,
value.messageId?.let { message ->
getString(message)
})
updateView(
warningView,
value.warningId?.let { warning ->
getString(warning)
})
cancelButton?.apply {
isVisible = value.cancelable != null
setOnClickListener {
value.cancelable?.invoke()
}
}
}
else -> {
// Nothing here, this fragment is stopped externally
}
} }
} }
} }

View File

@@ -4,30 +4,25 @@ import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.ProgressMessage import com.kunzisoft.keepass.database.ProgressMessage
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class ProgressTaskViewModel: ViewModel() { class ProgressTaskViewModel: ViewModel() {
private val mProgressMessageState = MutableStateFlow(ProgressMessage()) private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Hide)
val progressMessageState: StateFlow<ProgressMessage> = mProgressMessageState
private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Stop)
val progressTaskState: StateFlow<ProgressTaskState> = mProgressTaskState val progressTaskState: StateFlow<ProgressTaskState> = mProgressTaskState
fun update(value: ProgressMessage) { fun show(value: ProgressMessage) {
mProgressMessageState.value = value mProgressTaskState.update { currentState ->
ProgressTaskState.Show(value)
}
} }
fun start(value: ProgressMessage) { fun hide() {
mProgressTaskState.value = ProgressTaskState.Start mProgressTaskState.value = ProgressTaskState.Hide
update(value)
}
fun stop() {
mProgressTaskState.value = ProgressTaskState.Stop
} }
sealed class ProgressTaskState { sealed class ProgressTaskState {
object Start: ProgressTaskState() data class Show(val value: ProgressMessage): ProgressTaskState()
object Stop: ProgressTaskState() object Hide: ProgressTaskState()
} }
} }

View File

@@ -32,7 +32,7 @@ class DatabaseViewModel(application: Application): AndroidViewModel(application)
val database: ContextualDatabase? val database: ContextualDatabase?
get() = databaseState.value get() = databaseState.value
private val mActionState = MutableStateFlow<ActionState>(ActionState.Loading) private val mActionState = MutableStateFlow<ActionState>(ActionState.Wait)
val actionState: StateFlow<ActionState> = mActionState val actionState: StateFlow<ActionState> = mActionState
private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider( private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
@@ -469,7 +469,7 @@ class DatabaseViewModel(application: Application): AndroidViewModel(application)
} }
sealed class ActionState { sealed class ActionState {
object Loading: ActionState() object Wait: ActionState()
object OnDatabaseReloaded: ActionState() object OnDatabaseReloaded: ActionState()
data class OnDatabaseActionRequested( data class OnDatabaseActionRequested(
val bundle: Bundle? = null, val bundle: Bundle? = null,

View File

@@ -71,7 +71,7 @@ object AppOriginEntryField {
/** /**
* Useful to detect if an other KeePass compatibility app already add a web domain or an app id * Useful to detect if an other KeePass compatibility app already add a web domain or an app id
*/ */
private fun EntryInfo.containsDomainOrApplicationId(search: String): Boolean { fun EntryInfo.containsDomainOrApplicationId(search: String): Boolean {
if (url.contains(search)) if (url.contains(search))
return true return true
return customFields.find { return customFields.find {

View File

@@ -27,6 +27,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.Tags import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.model.AppOriginEntryField.containsDomainOrApplicationId
import com.kunzisoft.keepass.model.AppOriginEntryField.setAppOrigin import com.kunzisoft.keepass.model.AppOriginEntryField.setAppOrigin
import com.kunzisoft.keepass.model.AppOriginEntryField.setApplicationId import com.kunzisoft.keepass.model.AppOriginEntryField.setApplicationId
import com.kunzisoft.keepass.model.AppOriginEntryField.setWebDomain import com.kunzisoft.keepass.model.AppOriginEntryField.setWebDomain
@@ -183,6 +184,18 @@ class EntryInfo : NodeInfo {
} }
} }
/**
* True if this entry contains domain or applicationId,
* OTP is ignored and considered not present
*/
fun containsSearchInfo(searchInfo: SearchInfo): Boolean {
return searchInfo.webDomain?.let { webDomain ->
containsDomainOrApplicationId(webDomain)
} ?: searchInfo.applicationId?.let { applicationId ->
containsDomainOrApplicationId(applicationId)
} ?: false
}
/** /**
* Add searchInfo to current EntryInfo * Add searchInfo to current EntryInfo
*/ */

View File

@@ -0,0 +1,4 @@
* Fix multiple Passkey selection #2253
* Fix database dialog subtitle #2254
* Fix save search info if URL present #2255
* Small fixes

View File

@@ -0,0 +1,4 @@
* Correction de la selection multiple des Passkeys #2253
* Correction du sous-titre du dialogue de la base de données #2254
* Correction de la sauvegarde des infos de recherchesi l'URL est present #2255
* Petites corrections