Compare commits

..

8 Commits
4.2.2 ... 4.2.3

Author SHA1 Message Date
J-Jamet
e3b69789bf Merge branch 'release/4.2.3' 2025-10-29 18:37:19 +01:00
J-Jamet
54f2ed9fab fix: Small fixes 2025-10-29 18:27:54 +01:00
J-Jamet
2fea019b95 fix: Save search info if URL present #2255 2025-10-29 18:12:44 +01:00
J-Jamet
9ac7ef2d22 fix: Credential allow action during orientation change #2253 2025-10-29 17:12:27 +01:00
J-Jamet
6d452fa49c fix: Credential allow action #2253 2025-10-29 16:44:48 +01:00
J-Jamet
d99edb6b4d fix: database dialog subtitle #2254 2025-10-29 16:18:51 +01:00
J-Jamet
cb679f0d59 fix: Multiple Passkey selection #2253 2025-10-29 15:15:44 +01:00
J-Jamet
5dd9f75095 Merge tag '4.2.2' into develop
4.2.2
2025-10-28 17:44:51 +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)
* Fix database merge algorithm #2223
* Fix save search info #2243

View File

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

View File

@@ -841,7 +841,7 @@ class GroupActivity : DatabaseLockActivity(),
// Open child group
loadMainGroup(GroupState(group.nodeId, 0))
} 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 {
@@ -867,6 +867,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!database.isReadOnly
&& searchInfo != null
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
&& entryVersioned.containsSearchInfo(database, searchInfo).not()
) {
updateEntryWithRegisterInfo(
database,
@@ -884,6 +885,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!database.isReadOnly
&& searchInfo != null
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
&& entryVersioned.containsSearchInfo(database, searchInfo).not()
) {
updateEntryWithRegisterInfo(
database,
@@ -912,7 +914,7 @@ class GroupActivity : DatabaseLockActivity(),
finish()
})
} 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)
}
private fun Entry.containsSearchInfo(
database: ContextualDatabase,
searchInfo: SearchInfo
): Boolean {
return getEntryInfo(
database,
raw = true,
removeTemplateConfiguration = false
).containsSearchInfo(searchInfo)
}
private fun finishNodeAction() {
actionNodeMode?.finish()
}

View File

@@ -1,13 +1,17 @@
package com.kunzisoft.keepass.activities.legacy
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
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.Companion.DATABASE_CHANGED_DIALOG_TAG
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.DatabaseTaskProvider.Companion.startDatabaseService
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?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
) {
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
if (credentialResultLaunched.not()) {
when (uiState) {
is DatabaseViewModel.ActionState.Loading -> {}
is DatabaseViewModel.ActionState.Wait -> {}
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
if (finishActivityIfReloadRequested()) {
finish()
@@ -82,13 +141,13 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
progressTaskViewModel.start(uiState.progressMessage)
progressTaskViewModel.show(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
progressTaskViewModel.update(uiState.progressMessage)
progressTaskViewModel.show(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
progressTaskViewModel.stop()
progressTaskViewModel.hide()
}
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
@@ -96,7 +155,8 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
uiState.actionTask,
uiState.result
)
progressTaskViewModel.stop()
progressTaskViewModel.hide()
}
}
}
}
@@ -106,9 +166,9 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
progressTaskViewModel.progressTaskState.collect { state ->
when (state) {
ProgressTaskViewModel.ProgressTaskState.Start ->
showDialog()
ProgressTaskViewModel.ProgressTaskState.Stop ->
is ProgressTaskViewModel.ProgressTaskState.Show ->
startDialog()
is ProgressTaskViewModel.ProgressTaskState.Hide ->
stopDialog()
}
}
@@ -117,6 +177,7 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
if (credentialResultLaunched.not()) {
// Nullable function
onUnknownDatabaseRetrieved(database)
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
@@ -207,7 +274,7 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
}
}
private fun showDialog() {
private fun startDialog() {
lifecycleScope.launch {
if (showDatabaseDialog()) {
if (progressTaskDialogFragment == null) {
@@ -233,4 +300,8 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected open fun showDatabaseDialog(): Boolean {
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
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
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.retrieveSpecialMode
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.TypeMode
import com.kunzisoft.keepass.model.RegisterInfo
@@ -34,21 +30,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
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() {
if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode()

View File

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

View File

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

View File

@@ -70,16 +70,35 @@ open class ProgressTaskDialogFragment : DialogFragment() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
progressTaskViewModel.progressMessageState.collect { state ->
updateView(titleView,
state.titleId?.let { title -> getString(title) })
updateView(messageView,
state.messageId?.let { message -> getString(message) })
updateView(warningView,
state.warningId?.let { warning -> getString(warning) })
cancelButton?.isVisible = state.cancelable != null
cancelButton?.setOnClickListener {
state.cancelable?.invoke()
progressTaskViewModel.progressTaskState.collect { state ->
when (state) {
is ProgressTaskViewModel.ProgressTaskState.Show -> {
val value = state.value
updateView(
titleView,
value.titleId?.let { title ->
getString(title)
})
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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class ProgressTaskViewModel: ViewModel() {
private val mProgressMessageState = MutableStateFlow(ProgressMessage())
val progressMessageState: StateFlow<ProgressMessage> = mProgressMessageState
private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Stop)
private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Hide)
val progressTaskState: StateFlow<ProgressTaskState> = mProgressTaskState
fun update(value: ProgressMessage) {
mProgressMessageState.value = value
fun show(value: ProgressMessage) {
mProgressTaskState.update { currentState ->
ProgressTaskState.Show(value)
}
}
fun start(value: ProgressMessage) {
mProgressTaskState.value = ProgressTaskState.Start
update(value)
}
fun stop() {
mProgressTaskState.value = ProgressTaskState.Stop
fun hide() {
mProgressTaskState.value = ProgressTaskState.Hide
}
sealed class ProgressTaskState {
object Start: ProgressTaskState()
object Stop: ProgressTaskState()
data class Show(val value: ProgressMessage): ProgressTaskState()
object Hide: ProgressTaskState()
}
}

View File

@@ -32,7 +32,7 @@ class DatabaseViewModel(application: Application): AndroidViewModel(application)
val database: ContextualDatabase?
get() = databaseState.value
private val mActionState = MutableStateFlow<ActionState>(ActionState.Loading)
private val mActionState = MutableStateFlow<ActionState>(ActionState.Wait)
val actionState: StateFlow<ActionState> = mActionState
private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
@@ -469,7 +469,7 @@ class DatabaseViewModel(application: Application): AndroidViewModel(application)
}
sealed class ActionState {
object Loading: ActionState()
object Wait: ActionState()
object OnDatabaseReloaded: ActionState()
data class OnDatabaseActionRequested(
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
*/
private fun EntryInfo.containsDomainOrApplicationId(search: String): Boolean {
fun EntryInfo.containsDomainOrApplicationId(search: String): Boolean {
if (url.contains(search))
return true
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.Tags
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.setApplicationId
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
*/

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