diff --git a/CHANGELOG b/CHANGELOG index b296c0875..74e64e954 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/app/build.gradle b/app/build.gradle index c154fe923..e6bce9972 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 0b37f2378..94ad52ef6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -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() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt index ad4ce1f08..d07e7cf8f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt @@ -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,49 +59,104 @@ 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 + ) : ActivityResultLauncher() { + + override fun launch( + input: Intent?, + options: ActivityOptionsCompat? + ) { + credentialResultLaunched = true + builder.launch(input, options) + } + + override fun unregister() { + builder.unregister() + } + + override fun getContract(): ActivityResultContract { + 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 -> - when (uiState) { - is DatabaseViewModel.ActionState.Loading -> {} - is DatabaseViewModel.ActionState.OnDatabaseReloaded -> { - if (finishActivityIfReloadRequested()) { - finish() + if (credentialResultLaunched.not()) { + when (uiState) { + is DatabaseViewModel.ActionState.Wait -> {} + is DatabaseViewModel.ActionState.OnDatabaseReloaded -> { + if (finishActivityIfReloadRequested()) { + finish() + } } - } - is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> { - if (manageDatabaseInfo()) { - showDatabaseChangedDialog( - uiState.previousDatabaseInfo, - uiState.newDatabaseInfo, - uiState.readOnlyDatabase + is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> { + if (manageDatabaseInfo()) { + showDatabaseChangedDialog( + uiState.previousDatabaseInfo, + uiState.newDatabaseInfo, + uiState.readOnlyDatabase + ) + } + } + is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> { + startDatabasePermissionService( + uiState.bundle, + uiState.actionTask ) } - } - is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> { - startDatabasePermissionService( - uiState.bundle, - uiState.actionTask - ) - } - is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> { - progressTaskViewModel.start(uiState.progressMessage) - } - is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> { - progressTaskViewModel.update(uiState.progressMessage) - } - is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> { - progressTaskViewModel.stop() - } - is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> { - onDatabaseActionFinished( - uiState.database, - uiState.actionTask, - uiState.result - ) - progressTaskViewModel.stop() + is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> { + progressTaskViewModel.show(uiState.progressMessage) + } + is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> { + progressTaskViewModel.show(uiState.progressMessage) + } + is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> { + progressTaskViewModel.hide() + } + is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> { + onDatabaseActionFinished( + uiState.database, + uiState.actionTask, + uiState.result + ) + 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,16 +177,23 @@ abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { mDatabaseViewModel.databaseState.collect { database -> - // Nullable function - onUnknownDatabaseRetrieved(database) - database?.let { - onDatabaseRetrieved(database) + if (credentialResultLaunched.not()) { + // Nullable function + onUnknownDatabaseRetrieved(database) + database?.let { + onDatabaseRetrieved(database) + } } } } } } + 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" + } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt index 4f9860753..ee2b03ac1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt @@ -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? = - registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { - setActivityResult( - lockDatabase = false, - resultCode = it.resultCode, - data = it.data - ) - } - open fun onDatabaseBackPressed() { if (mSpecialMode != SpecialMode.DEFAULT) onCancelSpecialMode() diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt index 448338dad..efa4d236f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt @@ -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() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index 2bcda248b..82e69845a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -661,7 +661,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } private fun updateMessage(resId: Int) { - mProgressMessage.messageId = resId + mProgressMessage = mProgressMessage.copy( + messageId = resId + ) notifyProgressMessage() } diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt index bd342976e..e0ee59e45 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt @@ -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 + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskViewModel.kt index f8812f682..0e5585291 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskViewModel.kt @@ -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 = mProgressMessageState - - private val mProgressTaskState = MutableStateFlow(ProgressTaskState.Stop) + private val mProgressTaskState = MutableStateFlow(ProgressTaskState.Hide) val progressTaskState: StateFlow = 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt index 200b88f7d..3b9b5a97c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt @@ -32,7 +32,7 @@ class DatabaseViewModel(application: Application): AndroidViewModel(application) val database: ContextualDatabase? get() = databaseState.value - private val mActionState = MutableStateFlow(ActionState.Loading) + private val mActionState = MutableStateFlow(ActionState.Wait) val actionState: StateFlow = 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, diff --git a/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt b/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt index bc9580458..567424e31 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt @@ -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 { diff --git a/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt index 8c45ee341..5576b664f 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt @@ -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 */ diff --git a/fastlane/metadata/android/en-US/changelogs/148.txt b/fastlane/metadata/android/en-US/changelogs/148.txt new file mode 100644 index 000000000..35bfcaf8c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/148.txt @@ -0,0 +1,4 @@ + * Fix multiple Passkey selection #2253 + * Fix database dialog subtitle #2254 + * Fix save search info if URL present #2255 + * Small fixes \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/148.txt b/fastlane/metadata/android/fr-FR/changelogs/148.txt new file mode 100644 index 000000000..a9b9d6211 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/148.txt @@ -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 \ No newline at end of file