Compare commits

...

24 Commits

Author SHA1 Message Date
J-Jamet
c88413f7f7 feat: Add UV behind database merge #2283 2025-12-04 12:35:42 +01:00
J-Jamet
7b1fb8a4bf fix: save instance state in protected view #2283 2025-12-04 11:32:07 +01:00
J-Jamet
3567fa797b fix: Copy protected field behind UV #2283 2025-12-03 15:04:35 +01:00
J-Jamet
eb41233e57 fix: ProtectField #2283 2025-12-03 12:43:27 +01:00
J-Jamet
b394a99e40 fix: Unprotect with User Verification #2283 2025-12-02 20:25:44 +01:00
J-Jamet
2bbb40e513 fix: Add ProtectedFieldView callback #2283 2025-12-02 19:36:46 +01:00
J-Jamet
09ef69e6ae fix: Add user verification device credential setting #2283 2025-12-02 16:55:42 +01:00
J-Jamet
762ac8f77b fix: Merge dialog #2283 2025-12-02 11:41:21 +01:00
J-Jamet
d28087d8d8 fix: User Verification dialog #2283 2025-12-02 10:06:00 +01:00
J-Jamet
17d4c363ac fix: Add User Verification to database settings #2283 2025-12-01 21:01:08 +01:00
J-Jamet
c754b6a049 Added a dialog to verify the password #2283 2025-12-01 18:40:38 +01:00
J-Jamet
9c6241afc9 fix: Behavior when ask device credential #2283 2025-12-01 13:20:06 +01:00
J-Jamet
f6774b6d51 feat: Dialog to ask device credential #2283 2025-11-29 15:36:53 +01:00
J-Jamet
108a61905e fix: Add condition for User Verification #2283 2025-11-29 13:30:11 +01:00
J-Jamet
d251788b1a fix: Add User Verification for Entry Edition #2283 2025-11-29 13:04:01 +01:00
J-Jamet
7ed8a44168 fix: Remove hide password setting #2283 2025-11-28 15:05:13 +01:00
J-Jamet
844b1dfc79 fix: Add main credential check method 2025-11-27 20:00:20 +01:00
J-Jamet
d087fcc930 fix: Add MainCredentialViewModel 2025-11-27 16:20:44 +01:00
J-Jamet
5fd25c6150 fix: Passkey subdomain #2291 2025-11-26 12:31:53 +01:00
J-Jamet
c1cfddddbe Merge branch 'develop' into feature/UserVerification 2025-11-26 11:23:31 +01:00
J-Jamet
609b536898 fix: User Verified flag during registration #2283 2025-11-26 10:16:51 +01:00
J-Jamet
f9051ce787 fix: Remove test line #2283 2025-11-25 19:22:40 +01:00
J-Jamet
d90d175bd8 fix: User Verified response flag 2025-11-25 19:15:37 +01:00
J-Jamet
c17fba8ef7 feat: Add User Verification #2283 2025-11-25 17:38:00 +01:00
110 changed files with 1719 additions and 540 deletions

View File

@@ -1,5 +1,6 @@
KeePassDX(4.3.0)
* Manual change of app language #1884 #1990
* Add Passkey User Verification #2283
* Fix autofill username detection #2276
* Fix Passkey in passwordless mode #2282

View File

@@ -41,6 +41,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
@@ -53,6 +56,11 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestShowUnprotectField
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
@@ -78,7 +86,10 @@ import com.kunzisoft.keepass.view.changeTitleColor
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.showError
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID
@@ -100,14 +111,10 @@ class EntryActivity : DatabaseLockActivity() {
private var loadingView: ProgressBar? = null
private val mEntryViewModel: EntryViewModel by viewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
private val mEntryActivityEducation = EntryActivityEducation(this)
private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false
private var mEntryLoaded = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentSelected: Attachment? = null
@@ -210,7 +217,7 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
}
} catch (e: ClassCastException) {
} catch (_: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
@@ -238,13 +245,9 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
if (entryInfoHistory != null) {
this.mMainEntryId = entryInfoHistory.mainEntryId
// Manage history position
val historyPosition = entryInfoHistory.historyPosition
this.mHistoryPosition = historyPosition
val entryIsHistory = historyPosition > -1
this.mEntryIsHistory = entryIsHistory
// Assign history dedicated view
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
// TODO History badge
@@ -279,7 +282,6 @@ class EntryActivity : DatabaseLockActivity() {
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
loadingView?.hideByFading()
mEntryLoaded = true
} else {
finish()
}
@@ -322,6 +324,73 @@ class EntryActivity : DatabaseLockActivity() {
)
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mEntryViewModel.entryState.collect { entryState ->
when (entryState) {
is EntryViewModel.EntryState.Loading -> {}
is EntryViewModel.EntryState.RequestUnprotectField -> {
mDatabase?.let { database ->
requestShowUnprotectField(
userVerificationViewModel = mUserVerificationViewModel,
database = database,
protectedFieldView = entryState.protectedFieldView
)
}
mEntryViewModel.actionPerformed()
}
is EntryViewModel.EntryState.RequestCopyProtectedField -> {
mDatabase?.let { database ->
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.COPY_PROTECTED_FIELD,
database = database,
field = entryState.field,
)
)
}
mEntryViewModel.actionPerformed()
}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.userVerificationState.collect { uVState ->
when (uVState) {
is UserVerificationViewModel.UVState.Loading -> {}
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
coordinatorLayout?.showError(uVState.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
val data = uVState.dataToVerify
when (data.actionType) {
UserVerificationActionType.SHOW_PROTECTED_FIELD -> {
// Unprotect field by its view
data.protectedFieldView?.unprotect()
}
UserVerificationActionType.COPY_PROTECTED_FIELD -> {
// Copy field value
data.field?.let {
mEntryViewModel.copyToClipboard(it)
}
}
UserVerificationActionType.EDIT_ENTRY -> {
// Edit Entry
editEntry(data.database, data.entryId)
}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
}
}
}
}
override fun finishActivityIfReloadRequested(): Boolean {
@@ -410,13 +479,13 @@ class EntryActivity : DatabaseLockActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
if (mEntryLoaded) {
if (mEntryViewModel.entryLoaded) {
val inflater = menuInflater
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu)
if (mEntryIsHistory && !mDatabaseReadOnly) {
if (mEntryViewModel.entryIsHistory && !mDatabaseReadOnly) {
inflater.inflate(R.menu.entry_history, menu)
}
@@ -429,7 +498,7 @@ class EntryActivity : DatabaseLockActivity() {
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
if (mEntryIsHistory || mDatabaseReadOnly) {
if (mEntryViewModel.entryIsHistory || mDatabaseReadOnly) {
menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
menu?.findItem(R.id.menu_edit)?.isVisible = false
@@ -474,34 +543,53 @@ class EntryActivity : DatabaseLockActivity() {
}
}
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
database?.let { database ->
entryId?.let { entryId ->
EntryEditActivity.launch(
activity = this@EntryActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->
EntryEditActivity.launch(
activity = this,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
if (mEntryViewModel.entryInfo?.isUserVerificationNeeded() == true) {
mDatabase?.let { database ->
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.EDIT_ENTRY,
database = database,
entryId = mEntryViewModel.mainEntryId
)
)
}
} else {
editEntry(mDatabase, mEntryViewModel.mainEntryId)
}
return true
}
R.id.menu_restore_entry_history -> {
mMainEntryId?.let { mainEntryId ->
mEntryViewModel.mainEntryId?.let { mainEntryId ->
restoreEntryHistory(
mainEntryId,
mHistoryPosition)
mEntryViewModel.historyPosition
)
}
}
R.id.menu_delete_entry_history -> {
mMainEntryId?.let { mainEntryId ->
mEntryViewModel.mainEntryId?.let { mainEntryId ->
deleteEntryHistory(
mainEntryId,
mHistoryPosition)
mEntryViewModel.historyPosition
)
}
}
R.id.menu_save_database -> {
@@ -521,7 +609,7 @@ class EntryActivity : DatabaseLockActivity() {
override fun finish() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntryViewModel.mainEntryId)
setResult(RESULT_OK, this)
}
super.finish()

View File

@@ -63,6 +63,8 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecia
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestShowUnprotectField
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
@@ -98,9 +100,11 @@ import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.showError
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID
@@ -129,6 +133,7 @@ class EntryEditActivity : DatabaseLockActivity(),
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
private var mAllowCustomFields = false
private var mAllowOTP = false
@@ -383,23 +388,52 @@ class EntryEditActivity : DatabaseLockActivity(),
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mEntryEditViewModel.uiState.collect { uiState ->
mEntryEditViewModel.entryEditState.collect { uiState ->
when (uiState) {
EntryEditViewModel.UIState.Loading -> {}
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
AlertDialog.Builder(this@EntryEditActivity)
.setTitle(R.string.warning_overwrite_data_title)
.setMessage(R.string.warning_overwrite_data_description)
.setNegativeButton(android.R.string.cancel) { _, _ ->
mEntryEditViewModel.backPressedAlreadyApproved = true
onCancelSpecialMode()
}
.setPositiveButton(android.R.string.ok) { _, _ ->
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
}
.create().show()
is EntryEditViewModel.EntryEditState.Loading -> {}
is EntryEditViewModel.EntryEditState.ShowOverwriteMessage -> {
AlertDialog.Builder(this@EntryEditActivity)
.setTitle(R.string.warning_overwrite_data_title)
.setMessage(R.string.warning_overwrite_data_description)
.setNegativeButton(android.R.string.cancel) { _, _ ->
mEntryEditViewModel.backPressedAlreadyApproved = true
onCancelSpecialMode()
}
.setPositiveButton(android.R.string.ok) { _, _ -> }
.create().show()
mEntryEditViewModel.actionPerformed()
}
is EntryEditViewModel.EntryEditState.RequestUnprotectField -> {
mDatabase?.let { database ->
requestShowUnprotectField(
userVerificationViewModel = mUserVerificationViewModel,
database = database,
protectedFieldView = uiState.protectedFieldView
)
}
mEntryEditViewModel.actionPerformed()
}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mUserVerificationViewModel.userVerificationState.collect { uVState ->
when (uVState) {
is UserVerificationViewModel.UVState.Loading -> {}
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
coordinatorLayout?.showError(uVState.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
when (uVState.dataToVerify.actionType) {
UserVerificationActionType.SHOW_PROTECTED_FIELD -> {
uVState.dataToVerify.protectedFieldView?.unprotect()
}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
}
@@ -448,7 +482,7 @@ class EntryEditActivity : DatabaseLockActivity(),
searchAction = {
// Nothing when search retrieved
},
selectionAction = { intentSender, typeMode, searchInfo ->
selectionAction = { _, typeMode, _ ->
when(typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD ->

View File

@@ -52,6 +52,9 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker
@@ -72,10 +75,13 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
@@ -117,10 +123,14 @@ import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.showError
import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
import org.joda.time.LocalDateTime
import java.util.EnumSet
@@ -130,8 +140,7 @@ class GroupActivity : DatabaseLockActivity(),
GroupFragment.NodesActionMenuListener,
GroupFragment.OnScrollListener,
GroupFragment.GroupRefreshedListener,
SortDialogFragment.SortSelectionListener,
MainCredentialDialogFragment.AskMainCredentialDialogListener {
SortDialogFragment.SortSelectionListener {
// Views
private var header: ViewGroup? = null
@@ -156,6 +165,8 @@ class GroupActivity : DatabaseLockActivity(),
private val mGroupViewModel: GroupViewModel by viewModels()
private val mGroupEditViewModel: GroupEditViewModel by viewModels()
private val mMainCredentialViewModel: MainCredentialViewModel by viewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
private val mGroupActivityEducation = GroupActivityEducation(this)
@@ -356,14 +367,22 @@ class GroupActivity : DatabaseLockActivity(),
SettingsActivity.launch(this@GroupActivity, true)
}
R.id.menu_merge_from -> {
mExternalFileHelper?.openDocument()
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.MERGE_FROM_DATABASE,
database = mDatabase
)
)
}
R.id.menu_save_copy_to -> {
mExternalFileHelper?.createDocument(
getString(R.string.database_file_name_default) +
"_" +
LocalDateTime.now().toString() +
mDatabase?.defaultFileExtension)
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.SAVE_DATABASE_COPY_TO,
database = mDatabase
)
)
}
R.id.menu_lock_all -> {
lockAndExit()
@@ -548,6 +567,58 @@ class GroupActivity : DatabaseLockActivity(),
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mMainCredentialViewModel.uiState.collect { credentialState ->
when (credentialState) {
is MainCredentialViewModel.UIState.Loading -> {}
is MainCredentialViewModel.UIState.OnMainCredentialEntered -> {
mergeDatabaseFrom(credentialState.databaseUri, credentialState.mainCredential)
mMainCredentialViewModel.onActionReceived()
}
is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> {
mMainCredentialViewModel.onActionReceived()
}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.userVerificationState.collect { uVState ->
when (uVState) {
is UserVerificationViewModel.UVState.Loading -> {}
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
coordinatorLayout?.showError(uVState.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
val data = uVState.dataToVerify
when (data.actionType) {
UserVerificationActionType.EDIT_ENTRY -> {
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
}
UserVerificationActionType.MERGE_FROM_DATABASE -> {
mExternalFileHelper?.openDocument()
}
UserVerificationActionType.SAVE_DATABASE_COPY_TO -> {
mExternalFileHelper?.createDocument(
getString(R.string.database_file_name_default) +
"_" +
LocalDateTime.now().toString() +
uVState.dataToVerify.database?.defaultFileExtension
)
}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
}
}
}
}
override fun viewToInvalidateTimeout(): View? {
@@ -668,14 +739,6 @@ class GroupActivity : DatabaseLockActivity(),
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
var entry: Entry? = null
try {
entry = result.data?.getNewEntry(database)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry action for selection", e)
}
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess) {
@@ -687,7 +750,13 @@ class GroupActivity : DatabaseLockActivity(),
searchAction = {
// Search not used
},
selectionAction = { intentSenderMode, typeMode, searchInfo ->
selectionAction = { _, typeMode, _ ->
var entry: Entry? = null
try {
entry = result.data?.getNewEntry(database)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry action for selection", e)
}
when (typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> entry?.let {
@@ -701,19 +770,17 @@ class GroupActivity : DatabaseLockActivity(),
}
}
},
registrationAction = { intentSenderMode, typeMode, searchInfo ->
registrationAction = { _, _, _ ->
// Save not used
}
)
}
coordinatorError?.showActionErrorIfNeeded(result)
// Reload the group
loadGroup()
finishNodeAction()
}
}
coordinatorError?.showActionErrorIfNeeded(result)
// Reload the group
loadGroup()
finishNodeAction()
}
private fun manageIntent(intent: Intent?) {
@@ -860,7 +927,7 @@ class GroupActivity : DatabaseLockActivity(),
searchAction = {
// Nothing here, a search is simply performed
},
selectionAction = { intentSenderMode, typeMode, searchInfo ->
selectionAction = { _, typeMode, searchInfo ->
when (typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> {
@@ -994,6 +1061,20 @@ class GroupActivity : DatabaseLockActivity(),
).containsSearchInfo(searchInfo)
}
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
database?.let {
entryId?.let {
EntryEditActivity.launch(
activity = this@GroupActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
}
private fun finishNodeAction() {
actionNodeMode?.finish()
}
@@ -1043,13 +1124,18 @@ class GroupActivity : DatabaseLockActivity(),
launchDialogForGroupUpdate(node as Group)
}
Type.ENTRY -> {
EntryEditActivity.launch(
activity = this@GroupActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = (node as Entry).nodeId,
activityResultLauncher = mEntryActivityResultLauncher
)
if ((node as Entry).getEntryInfo(database).isUserVerificationNeeded()) {
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.EDIT_ENTRY,
database = database,
entryId = node.nodeId
)
)
} else {
editEntry(database, node.nodeId)
}
}
}
return true
@@ -1133,20 +1219,6 @@ class GroupActivity : DatabaseLockActivity(),
return true
}
override fun onAskMainCredentialDialogPositiveClick(
databaseUri: Uri?,
mainCredential: MainCredential
) {
databaseUri?.let {
mergeDatabaseFrom(it, mainCredential)
}
}
override fun onAskMainCredentialDialogNegativeClick(
databaseUri: Uri?,
mainCredential: MainCredential
) { }
override fun onResume() {
super.onResume()
@@ -1644,13 +1716,13 @@ class GroupActivity : DatabaseLockActivity(),
context = activity,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
onItemsFound = { _, items ->
when (typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> {
MagikeyboardService.performSelection(
items = items,
actionPopulateKeyboard = { entryInfo ->
actionPopulateKeyboard = { _ ->
activity.buildSpecialModeResponseAndSetResult(items)
onValidateSpecialMode()
},

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.text.InputFilter
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.MasterCredential
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
class CheckDatabaseCredentialDialogFragment : DatabaseDialogFragment() {
private val userVerificationViewModel: UserVerificationViewModel by activityViewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val rootView = inflater.inflate(R.layout.fragment_check_database_credential, null)
val editText = rootView.findViewById<TextView>(R.id.setup_check_password_edit_text)
editText.filters = arrayOf<InputFilter>(InputFilter.LengthFilter(
MasterCredential.CHECK_KEY_PASSWORD_LENGTH)
)
builder.setView(rootView)
.setPositiveButton(R.string.check) { _, _ ->
userVerificationViewModel.checkMainCredential(
editText.text.toString()
)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
userVerificationViewModel.onUserVerificationFailed()
dismiss()
}
rootView.findViewById<View>(R.id.user_verification_information)?.setOnClickListener {
activity.openUrl(R.string.user_verification_explanation_url)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
companion object {
fun getInstance(): CheckDatabaseCredentialDialogFragment {
val fragment = CheckDatabaseCredentialDialogFragment()
val args = Bundle()
fragment.arguments = args
return fragment
}
}
}

View File

@@ -20,45 +20,27 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
class MainCredentialDialogFragment : DatabaseDialogFragment() {
private var mainCredentialView: MainCredentialView? = null
private var mListener: AskMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
interface AskMainCredentialDialogListener {
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
}
override fun onAttach(activity: Context) {
super.onAttach(activity)
try {
mListener = activity as AskMainCredentialDialogListener
} catch (e: ClassCastException) {
throw ClassCastException(activity.toString()
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
private val mMainCredentialViewModel: MainCredentialViewModel by activityViewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
@@ -76,30 +58,35 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
databaseUri?.let {
root.findViewById<TextView>(R.id.title_database)?.text =
it.getDocumentFile(requireContext())?.name
}
builder.setView(root)
builder.setView(root)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAskMainCredentialDialogPositiveClick(
databaseUri,
retrieveMainCredential()
mMainCredentialViewModel.validateMainCredential(
databaseUri = databaseUri,
mainCredential = retrieveMainCredential()
)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
mListener?.onAskMainCredentialDialogNegativeClick(
databaseUri,
retrieveMainCredential()
mMainCredentialViewModel.cancelMainCredential(
databaseUri = databaseUri
)
}
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileView(uri)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
} ?: run {
mMainCredentialViewModel.cancelMainCredential(
databaseUri = null,
error = FileNotFoundDatabaseException()
)
dismissAllowingStateLoss()
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
return builder.create()
}

View File

@@ -116,6 +116,9 @@ class EntryEditFragment: DatabaseFragment() {
setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
}
setOnUnprotectClickListener { _, textEditFieldView ->
mEntryEditViewModel.requestUnprotectField(textEditFieldView)
}
setOnCustomEditionActionClickListener { field ->
mEntryEditViewModel.requestCustomFieldEdition(field)
}

View File

@@ -16,13 +16,10 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.view.TemplateView
@@ -50,8 +47,6 @@ class EntryFragment: DatabaseFragment() {
private lateinit var uuidContainerView: View
private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null
private val mEntryViewModel: EntryViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
@@ -66,10 +61,6 @@ class EntryFragment: DatabaseFragment() {
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { context ->
mClipboardHelper = ClipboardHelper(context)
}
rootView = view
// Hide only the first time
if (savedInstanceState == null) {
@@ -152,16 +143,14 @@ class EntryFragment: DatabaseFragment() {
private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Set copy buttons
templateView.apply {
setOnUnprotectClickListener { protectedFieldView ->
mEntryViewModel.requestUnprotectField(protectedFieldView)
}
setOnAskCopySafeClickListener {
showClipboardDialog()
}
setOnCopyActionClickListener { field ->
mClipboardHelper?.timeoutCopyToClipboard(
TemplateField.getLocalizedName(context, field.name),
field.protectedValue.stringValue,
field.protectedValue.isProtected
)
setOnCopyActionClickListener { field, protectedFieldView ->
mEntryViewModel.requestCopyField(field, protectedFieldView)
}
}
@@ -242,14 +231,14 @@ class EntryFragment: DatabaseFragment() {
fun firstEntryFieldCopyView(): View? {
return try {
templateView.getActionImageView()
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
fun launchEntryCopyEducationAction() {
val appNameString = getString(R.string.app_name)
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
mEntryViewModel.copyToClipboard(appNameString)
}
companion object {

View File

@@ -0,0 +1,25 @@
package com.kunzisoft.keepass.credentialprovider
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.view.ProtectedFieldView
data class UserVerificationData(
val actionType: UserVerificationActionType,
val database: ContextualDatabase? = null,
val entryId: NodeId<*>? = null,
val field: Field? = null,
val protectedFieldView: ProtectedFieldView? = null,
val preferenceKey: String? = null
)
enum class UserVerificationActionType {
LAUNCH_PASSKEY_CEREMONY,
SHOW_PROTECTED_FIELD,
COPY_PROTECTED_FIELD,
EDIT_ENTRY,
EDIT_DATABASE_SETTING,
MERGE_FROM_DATABASE,
SAVE_DATABASE_COPY_TO
}

View File

@@ -0,0 +1,209 @@
package com.kunzisoft.keepass.credentialprovider
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.CheckDatabaseCredentialDialogFragment
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.isUserVerificationDeviceCredential
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.putEnumExtra
import com.kunzisoft.keepass.view.ProtectedFieldView
import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
class UserVerificationHelper {
companion object {
private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification"
private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth"
/**
* Allowed authenticators for the User Verification
*/
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL
/**
* Check if the device supports the biometric prompt for User Verification
*/
fun Context.isAuthenticatorsAllowed(): Boolean {
return BiometricManager.from(this)
.canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS
}
/**
* Add the User Verification to the intent
*/
fun Intent.addUserVerification(
userVerification: UserVerificationRequirement,
userVerifiedWithAuth: Boolean
) {
putEnumExtra(EXTRA_USER_VERIFICATION, userVerification)
putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth)
}
/**
* Define if the User is verified with authentification from the intent
*/
fun Intent.getUserVerifiedWithAuth(): Boolean {
return getBooleanExtra(EXTRA_USER_VERIFIED_WITH_AUTH, true)
}
/**
* Remove the User Verification from the intent
*/
fun Intent.removeUserVerification() {
removeExtra(EXTRA_USER_VERIFICATION)
}
/**
* Remove the User verified with auth from the intent
*/
fun Intent.removeUserVerifiedWithAuth() {
removeExtra(EXTRA_USER_VERIFIED_WITH_AUTH)
}
/**
* Get the User Verification from the intent
*/
fun Intent.isUserVerificationNeeded(userVerificationPreferred: Boolean): Boolean {
val userVerification: UserVerificationRequirement =
getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
?: UserVerificationRequirement.PREFERRED
return (userVerification == UserVerificationRequirement.REQUIRED
|| (userVerificationPreferred
&& userVerification == UserVerificationRequirement.PREFERRED)
)
}
/**
* Check if the User needs to be verified for this entry
*/
fun EntryInfo.isUserVerificationNeeded(): Boolean {
// Apply to any entry with protected content
// Not only this.passkey != null
return true
}
fun Fragment.checkUserVerification(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData
) {
activity?.checkUserVerification(userVerificationViewModel, dataToVerify)
}
fun FragmentActivity.requestShowUnprotectField(
userVerificationViewModel: UserVerificationViewModel,
database: ContextualDatabase,
protectedFieldView: ProtectedFieldView
) {
if (protectedFieldView.isCurrentlyProtected()) {
checkUserVerification(
userVerificationViewModel = userVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.SHOW_PROTECTED_FIELD,
database = database,
protectedFieldView = protectedFieldView
)
)
} else {
protectedFieldView.protect()
}
}
/**
* Displays a dialog to verify the user
*/
fun FragmentActivity.checkUserVerification(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData
) {
if (isAuthenticatorsAllowed() && isUserVerificationDeviceCredential(this)) {
showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
} else if (dataToVerify.database != null) {
showUserVerificationDatabaseCredential(userVerificationViewModel, dataToVerify)
}
}
/**
* Displays a dialog for entering the device credential to be checked
*/
fun FragmentActivity.showUserVerificationDeviceCredential(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData
) {
BiometricPrompt(
this, ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
super.onAuthenticationError(errorCode, errString)
when (errorCode) {
BiometricPrompt.ERROR_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
BiometricPrompt.ERROR_USER_CANCELED -> {
// No operation
Log.i("UserVerification", "$errString")
}
else -> {
toastError(SecurityException("Authentication error: $errString"))
}
}
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
toastError(SecurityException(getString(R.string.device_unlock_not_recognized)))
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
}
}).authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.user_verification_required_title))
.setSubtitle(getString(R.string.user_verification_required_description))
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
.setConfirmationRequired(false)
.build()
)
}
/**
* Displays a dialog for entering the database credential to be checked
*/
fun FragmentActivity.showUserVerificationDatabaseCredential(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData
) {
userVerificationViewModel.dataToVerify = dataToVerify
val fragmentTag = "checkDatabaseCredentialDialog"
var fragment: CheckDatabaseCredentialDialogFragment? =
supportFragmentManager.findFragmentByTag(fragmentTag)
as? CheckDatabaseCredentialDialogFragment?
if (fragment == null) {
fragment = CheckDatabaseCredentialDialogFragment.getInstance()
fragment.show(this.supportFragmentManager, fragmentTag)
}
}
}
}

View File

@@ -31,7 +31,9 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
@@ -43,7 +45,14 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerifiedWithAuth
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
@@ -52,9 +61,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.settings.PreferencesUtil.isUserVerificationPreferred
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
import java.util.UUID
@@ -62,6 +73,7 @@ import java.util.UUID
class PasskeyLauncherActivity : DatabaseLockActivity() {
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
private val userVerificationViewModel: UserVerificationViewModel by viewModels()
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -83,9 +95,10 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Initialize the parameters
passkeyLauncherViewModel.initialize()
passkeyLauncherViewModel.initialize(userVerified = intent.getUserVerifiedWithAuth())
// Retrieve the UI
passkeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
@@ -161,11 +174,58 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
userVerificationViewModel.userVerificationState.collect { uiState ->
when (uiState) {
is UserVerificationViewModel.UVState.Loading -> {}
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
val data = uiState.dataToVerify
when (data.actionType) {
UserVerificationActionType.LAUNCH_PASSKEY_CEREMONY -> {
passkeyLauncherViewModel.launchActionIfNeeded(
userVerified = true,
intent = intent,
specialMode = mSpecialMode,
database = uiState.dataToVerify.database
)
}
else -> {}
}
userVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
userVerificationViewModel.onUserVerificationReceived()
}
}
}
}
}
}
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
val userVerificationNeeded = intent.isUserVerificationNeeded(
userVerificationPreferred = isUserVerificationPreferred(this)
) && intent.getUserVerifiedWithAuth().not()
if (userVerificationNeeded) {
checkUserVerification(
userVerificationViewModel = userVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.LAUNCH_PASSKEY_CEREMONY,
database = database
)
)
} else {
passkeyLauncherViewModel.launchActionIfNeeded(
intent = intent,
specialMode = mSpecialMode,
database = database
)
}
}
override fun onDatabaseActionFinished(
@@ -278,7 +338,9 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
specialMode: SpecialMode,
searchInfo: SearchInfo? = null,
appOrigin: AppOrigin? = null,
nodeId: UUID? = null
nodeId: UUID? = null,
userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED,
userVerifiedWithAuth: Boolean = true
): PendingIntent? {
return PendingIntent.getActivity(
context,
@@ -290,6 +352,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
addAppOrigin(appOrigin)
addNodeId(nodeId)
addAuthCode(nodeId)
addUserVerification(userVerification, userVerifiedWithAuth)
},
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)

View File

@@ -49,6 +49,7 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
@@ -149,7 +150,9 @@ class PasskeyProviderService : CredentialProviderService() {
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
.map { b64Encode(it.id) }
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialIds $credentialIdList")
val userVerification = publicKeyCredentialRequestOptions.userVerification
Log.d(TAG, "Build passkey search for UV $userVerification, " +
"RP $relyingPartyId and Credential IDs $credentialIdList")
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
@@ -161,14 +164,19 @@ class PasskeyProviderService : CredentialProviderService() {
context = applicationContext,
specialMode = SpecialMode.SELECTION,
nodeId = passkeyEntry.id,
appOrigin = passkeyEntry.appOrigin
appOrigin = passkeyEntry.appOrigin,
userVerification = userVerification,
userVerifiedWithAuth = false
)?.let { usagePendingIntent ->
val passkey = passkeyEntry.passkey
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = passkey?.username ?: "Unknown",
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
icon = passkeyEntry.buildIcon(
this@PasskeyProviderService,
database
)?.apply {
setTintBlendMode(BlendMode.DST)
} ?: defaultIcon,
pendingIntent = usagePendingIntent,
@@ -188,7 +196,9 @@ class PasskeyProviderService : CredentialProviderService() {
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.SELECTION,
searchInfo = searchInfo
searchInfo = searchInfo,
userVerification = userVerification,
userVerifiedWithAuth = false
)?.let { pendingIntent ->
passkeyEntries.add(
PublicKeyCredentialEntry(
@@ -220,7 +230,8 @@ class PasskeyProviderService : CredentialProviderService() {
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.SELECTION,
searchInfo = searchInfo
searchInfo = searchInfo,
userVerifiedWithAuth = true
)?.let { pendingIntent ->
passkeyEntries.add(
PublicKeyCredentialEntry(
@@ -275,14 +286,17 @@ class PasskeyProviderService : CredentialProviderService() {
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
accountName: String,
searchInfo: SearchInfo?
searchInfo: SearchInfo?,
userVerification: UserVerificationRequirement
) {
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
// TODO add a setting to directly store in a specific group
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.REGISTRATION,
searchInfo = searchInfo
searchInfo = searchInfo,
userVerification = userVerification,
userVerifiedWithAuth = false
)?.let { pendingIntent ->
this.add(
CreateEntry(
@@ -311,6 +325,7 @@ class PasskeyProviderService : CredentialProviderService() {
)
val relyingPartyId = publicKeyCredentialCreationOptions.relyingPartyEntity.id
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
val userVerification = publicKeyCredentialCreationOptions.authenticatorSelection.userVerification
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
SearchHelper.checkAutoSearchInfo(
context = this,
@@ -321,7 +336,11 @@ class PasskeyProviderService : CredentialProviderService() {
throw RegisterInReadOnlyDatabaseException()
} else {
// To create a new entry
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
createEntries.addPendingIntentCreationNewEntry(
accountName = accountName,
searchInfo = searchInfo,
userVerification = userVerification
)
/* TODO Overwrite
// To select an existing entry and permit an overwrite
Log.w(TAG, "Passkey already registered")
@@ -352,7 +371,11 @@ class PasskeyProviderService : CredentialProviderService() {
if (database.isReadOnly) {
throw RegisterInReadOnlyDatabaseException()
} else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
createEntries.addPendingIntentCreationNewEntry(
accountName = accountName,
searchInfo = searchInfo,
userVerification = userVerification
)
}
callback(createEntries)
},
@@ -361,7 +384,8 @@ class PasskeyProviderService : CredentialProviderService() {
Log.d(TAG, "Add pending intent for passkey registration in closed database")
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.REGISTRATION
specialMode = SpecialMode.REGISTRATION,
userVerifiedWithAuth = true
)?.let { pendingIntent ->
createEntries.add(
CreateEntry(

View File

@@ -148,11 +148,12 @@ data class PublicKeyCredentialDescriptor(
}
}
// https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria
data class AuthenticatorSelectionCriteria(
val authenticatorAttachment: String? = null,
val residentKey: ResidentKeyRequirement? = null,
val requireResidentKey: Boolean?,
val userVerification: UserVerificationRequirement? = UserVerificationRequirement.PREFERRED
val userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED
) {
companion object {
fun JSONObject.getAuthenticatorSelectionCriteria(
@@ -166,7 +167,9 @@ data class AuthenticatorSelectionCriteria(
ResidentKeyRequirement.fromString(authenticatorSelection.getString("residentKey"))
else null
val requireResidentKey = authenticatorSelection.optBoolean("requireResidentKey", false)
val userVerification = UserVerificationRequirement.fromString(authenticatorSelection.optString("userVerification", "preferred"))
val userVerification = UserVerificationRequirement
.fromString(authenticatorSelection.optString("userVerification", "preferred"))
?: UserVerificationRequirement.PREFERRED
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
if (residentKey == null) {
residentKey = if (requireResidentKey) {
@@ -195,7 +198,9 @@ enum class ResidentKeyRequirement(val value: String) {
}
companion object {
fun fromString(value: String): ResidentKeyRequirement? {
return ResidentKeyRequirement.entries.firstOrNull { it.value == value }
return ResidentKeyRequirement.entries.firstOrNull {
it.value.equals(other = value, ignoreCase = true)
}
}
}
}
@@ -210,7 +215,9 @@ enum class UserVerificationRequirement(val value: String) {
}
companion object {
fun fromString(value: String): UserVerificationRequirement? {
return UserVerificationRequirement.entries.firstOrNull { it.value == value }
return UserVerificationRequirement.entries.firstOrNull {
it.value.equals(other = value, ignoreCase = true)
}
}
}
}

View File

@@ -494,6 +494,7 @@ object PasskeyHelper {
*/
fun buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
userVerified: Boolean,
backupEligibility: Boolean,
backupState: Boolean
): CreatePublicKeyCredentialResponse {
@@ -511,7 +512,7 @@ object PasskeyHelper {
keyTypeId = keyTypeId
) ?: mapOf<Int, Any>()),
userPresent = true,
userVerified = true,
userVerified = userVerified,
backupEligibility = backupEligibility,
backupState = backupState,
publicKeyTypeId = keyTypeId,
@@ -583,6 +584,7 @@ object PasskeyHelper {
requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse,
passkey: Passkey,
userVerified: Boolean,
defaultBackupEligibility: Boolean,
defaultBackupState: Boolean
): PublicKeyCredential {
@@ -591,7 +593,7 @@ object PasskeyHelper {
response = AuthenticatorAssertionResponse(
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
userVerified = userVerified,
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle,

View File

@@ -24,7 +24,6 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
protected var isResultLauncherRegistered: Boolean = false
private var mSelectionResult: ActivityResult? = null
protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
@@ -56,7 +55,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
)
}
private fun onDatabaseRetrieved(database: ContextualDatabase) {
fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabase = database
mSelectionResult?.let { selectionResult ->
manageSelectionResult(database, selectionResult)

View File

@@ -64,14 +64,16 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
private var mPasskey: Passkey? = null
private var mLockDatabaseAfterSelection: Boolean = false
private var mUserVerified: Boolean = true
private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun initialize() {
fun initialize(userVerified: Boolean) {
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
mUserVerified = userVerified
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
}
@@ -149,6 +151,16 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
}
}
fun launchActionIfNeeded(
userVerified: Boolean,
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
this.mUserVerified = userVerified
launchActionIfNeeded(intent, specialMode, database)
}
override fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
@@ -307,6 +319,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
appOrigin = appOrigin
),
passkey = passkey,
userVerified = mUserVerified,
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
@@ -363,6 +376,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
appOrigin = appOrigin
),
passkey = passkey,
userVerified = mUserVerified,
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
@@ -505,6 +519,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it,
userVerified = mUserVerified,
backupEligibility = passkey?.backupEligibility
?: mBackupEligibility,
backupState = passkey?.backupState

View File

@@ -238,7 +238,7 @@ class DatabaseTaskProvider(
try {
context.unregisterReceiver(databaseTaskBroadcastReceiver)
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
// If receiver not register, do nothing
}
}
@@ -319,7 +319,6 @@ class DatabaseTaskProvider(
databaseUri: Uri,
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)

View File

@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.ProgressMessage
@@ -61,7 +62,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -917,7 +917,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseAssignCredentialActionTask(
intent: Intent,
database: ContextualDatabase,
database: ContextualDatabase
): ActionRunnable? {
return if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)

View File

@@ -302,7 +302,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
biometricUnlockEnablePreference.isChecked = false
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
biometricUnlockEnablePreference.isChecked = true
deviceCredentialUnlockEnablePreference?.isChecked = false
deviceCredentialUnlockEnablePreference.isChecked = false
}
} else {
biometricUnlockEnablePreference.isChecked = false
@@ -349,7 +349,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
deviceCredentialUnlockEnablePreference.isChecked = false
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
deviceCredentialUnlockEnablePreference.isChecked = true
biometricUnlockEnablePreference?.isChecked = false
biometricUnlockEnablePreference.isChecked = false
}
} else {
deviceCredentialUnlockEnablePreference.isChecked = false
@@ -412,7 +412,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
warningAlertDialog = AlertDialog.Builder(activity)
.setMessage(message)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(resources.getString(android.R.string.ok)
) { _, _ ->
validate?.invoke()
@@ -524,27 +523,23 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false
var dialogFragment: DialogFragment? = null
// Main Preferences
when (preference.key) {
getString(R.string.app_timeout_key),
getString(R.string.clipboard_timeout_key),
getString(R.string.temp_device_unlock_timeout_key) -> {
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
}
else -> otherDialogFragment = true
else -> {}
}
if (dialogFragment != null) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
}
// Could not be handled here. Try with the super method.
else if (otherDialogFragment) {
} else {
// Could not be handled here. Try with the super method.
super.onDisplayPreferenceDialog(preference)
}
}

View File

@@ -42,6 +42,9 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -74,11 +77,16 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import com.kunzisoft.keepass.viewmodels.SettingsViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
private val mSettingsViewModel: SettingsViewModel by activityViewModels()
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by activityViewModels()
private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
private var mDatabaseReadOnly: Boolean = false
@@ -171,6 +179,51 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.userVerificationState.collect { state ->
when (state) {
is UserVerificationViewModel.UVState.Loading -> {}
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
mSettingsViewModel.showError(state.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
val data = state.dataToVerify
when (data.actionType) {
UserVerificationActionType.EDIT_DATABASE_SETTING -> {
val database = data.database
val preferenceKey = data.preferenceKey
if (database != null && preferenceKey != null) {
// Main Preferences
when (preferenceKey) {
// Master Key
getString(R.string.settings_database_change_credentials_key) -> {
SetMainCredentialDialogFragment
.getInstance(database.allowNoMasterKey)
.show(parentFragmentManager, "passwordDialog")
}
else -> {}
}
// TODO Settings in compose
@Suppress("DEPRECATION")
mSettingsViewModel.dialogFragment?.let { dialogFragment ->
dialogFragment.setTargetFragment(
this@NestedDatabaseSettingsFragment, 0
)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
}
mSettingsViewModel.dialogFragment = null
}
}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -325,7 +378,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
// Change the recycle bin group
recycleBinGroupPref?.setOnPreferenceClickListener {
true
}
// Recycle Bin group
@@ -431,11 +483,18 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
private fun onCreateDatabaseMasterKeyPreference(database: ContextualDatabase) {
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
val changeCredentialKey = getString(R.string.settings_database_change_credentials_key)
findPreference<Preference>(changeCredentialKey)?.apply {
isEnabled = if (!mDatabaseReadOnly) {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
SetMainCredentialDialogFragment.getInstance(database.allowNoMasterKey)
.show(parentFragmentManager, "passwordDialog")
checkUserVerification(
mUserVerificationViewModel,
UserVerificationData(
actionType = UserVerificationActionType.EDIT_DATABASE_SETTING,
database = database,
preferenceKey = changeCredentialKey
)
)
false
}
true
@@ -462,7 +521,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
// To reassign color listener after orientation change
val chromaDialog = parentFragmentManager.findFragmentByTag(TAG_PREF_FRAGMENT) as DatabaseColorPreferenceDialogFragmentCompat?
chromaDialog?.onColorSelectedListener = colorSelectedListener
} catch (e: Exception) {}
} catch (_: Exception) {}
return view
}
@@ -730,9 +789,15 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
if (dialogFragment != null && !mDatabaseReadOnly) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
mSettingsViewModel.dialogFragment = dialogFragment
checkUserVerification(
mUserVerificationViewModel,
UserVerificationData(
actionType = UserVerificationActionType.EDIT_DATABASE_SETTING,
database = mDatabase,
preferenceKey = preference.key
)
)
}
// Could not be handled here. Try with the super method.
else if (otherDialogFragment) {
@@ -742,7 +807,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
override fun onResume() {
super.onResume()
context?.let { context ->
mDatabaseAutoSaveEnabled = PreferencesUtil.isAutoSaveDatabaseEnabled(context)
}

View File

@@ -30,11 +30,17 @@ import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFra
abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
enum class Screen {
APPLICATION, FORM_FILLING, DEVICE_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
APPLICATION,
FORM_FILLING,
DEVICE_UNLOCK,
APPEARANCE,
DATABASE,
DATABASE_SECURITY,
DATABASE_MASTER_KEY
}
fun getScreen(): Screen {
return Screen.values()[requireArguments().getInt(TAG_KEY)]
return Screen.entries[requireArguments().getInt(TAG_KEY)]
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -50,8 +56,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
preferenceInDev.setOnPreferenceClickListener { preference ->
try { // don't check if we can
(preference as TwoStatePreference).isChecked = false
} catch (ignored: Exception) {
}
} catch (_: Exception) {}
UnderDevelopmentFeatureDialogFragment().show(parentFragmentManager, "underDevFeatureDialog")
false
}

View File

@@ -132,12 +132,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.hide_templates_default))
}
fun hideProtectedValue(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.hide_password_key),
context.resources.getBoolean(R.bool.hide_password_default))
}
fun colorizePassword(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.colorize_password_key),
@@ -696,6 +690,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.passkeys_close_database_default))
}
fun isUserVerificationDeviceCredential(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.user_verification_device_credential_key),
context.resources.getBoolean(R.bool.user_verification_device_credential_default))
}
fun isUserVerificationPreferred(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.user_verification_preferred_key),
context.resources.getBoolean(R.bool.user_verification_preferred_default))
}
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
@@ -882,7 +888,6 @@ object PreferencesUtil {
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_templates_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())

View File

@@ -28,9 +28,13 @@ import android.view.MenuItem
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
@@ -41,6 +45,9 @@ import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.showError
import com.kunzisoft.keepass.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import java.util.Properties
@@ -49,6 +56,8 @@ open class SettingsActivity
MainPreferenceFragment.Callback,
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
private val mSettingsViewModel: SettingsViewModel by viewModels()
private var backupManager: BackupManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null
@@ -118,7 +127,7 @@ open class SettingsActivity
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
toolbar?.setTitle(R.string.settings)
else
toolbar?.title = savedInstanceState?.getString(TITLE_KEY)
toolbar?.title = savedInstanceState.getString(TITLE_KEY)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@@ -145,6 +154,20 @@ open class SettingsActivity
// Eat state
intent.removeExtra(FRAGMENT_ARG)
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mSettingsViewModel.settingsState.collect { settingsState ->
when (settingsState) {
is SettingsViewModel.SettingsState.Wait -> {}
is SettingsViewModel.SettingsState.ShowError -> {
coordinatorLayout?.showError(settingsState.error)
mSettingsViewModel.errorShown()
}
}
}
}
}
}
/**

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.text.Editable
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.text.TextWatcher
@@ -51,7 +50,6 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
private var mViewHint: String = ""
private var mMaxLines: Int = 3
private var mShowPassword: Boolean = false
private var mPasswordTextWatchers: MutableList<TextWatcher> = mutableListOf()
private var mPasswordTextWatcher: TextWatcher? = null
@@ -65,8 +63,6 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
mViewHint = getString(R.styleable.PasswordView_passwordHint)
?: context.getString(R.string.password)
mMaxLines = getInteger(R.styleable.PasswordView_passwordMaxLines, mMaxLines)
mShowPassword = getBoolean(R.styleable.PasswordView_passwordVisible,
!PreferencesUtil.hideProtectedValue(context))
} finally {
recycle()
}
@@ -76,16 +72,12 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
inflater?.inflate(R.layout.view_password_edit, this)
passwordInputLayout = findViewById(R.id.password_edit_input_layout)
passwordInputLayout?.hint = mViewHint
passwordInputLayout.hint = mViewHint
passwordText = findViewById(R.id.password_edit_text)
if (mShowPassword) {
passwordText?.inputType = passwordText.inputType or
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
}
passwordText?.maxLines = mMaxLines
passwordText?.applyFontVisibility()
passwordText.maxLines = mMaxLines
passwordText.applyFontVisibility()
passwordStrengthProgress = findViewById(R.id.password_edit_strength_progress)
passwordStrengthProgress?.apply {
passwordStrengthProgress.apply {
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
progress = 0
max = 100
@@ -93,7 +85,7 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
passwordEntropy = findViewById(R.id.password_edit_entropy)
mPasswordEntropyCalculator = PasswordEntropy {
passwordText?.text?.toString()?.let { firstPassword ->
passwordText.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
@@ -119,7 +111,7 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
PasswordGenerator.colorizedPassword(editable)
}
}
passwordText?.addTextChangedListener(mPasswordTextWatcher)
passwordText.addTextChangedListener(mPasswordTextWatcher)
}
private fun getEntropyStrength(passwordText: String) {

View File

@@ -0,0 +1,14 @@
package com.kunzisoft.keepass.view
import android.view.View.OnClickListener
interface ProtectedFieldView {
fun setProtection(
protection: Boolean,
isCurrentlyProtected: Boolean,
onUnprotectClickListener: OnClickListener?
)
fun isCurrentlyProtected(): Boolean
fun protect()
fun unprotect()
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.kunzisoft.keepass.utils.readBooleanCompat
import com.kunzisoft.keepass.utils.writeBooleanCompat
abstract class ProtectedTextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle),
GenericTextFieldView, ProtectedFieldView {
var isProtected: Boolean = false
private set
private var mIsCurrentlyProtected: Boolean = true
// Only to fix rebuild view from template
var onSaveInstanceState: (() -> Unit)? = null
override fun isCurrentlyProtected(): Boolean {
return mIsCurrentlyProtected
}
override fun protect() {
mIsCurrentlyProtected = true
changeProtectedValueParameters()
}
override fun unprotect() {
mIsCurrentlyProtected = false
changeProtectedValueParameters()
}
override fun setProtection(
protection: Boolean,
isCurrentlyProtected: Boolean,
onUnprotectClickListener: OnClickListener?
) {
this.isProtected = protection
this.mIsCurrentlyProtected = isCurrentlyProtected
if (isProtected) {
changeProtectedValueParameters()
}
}
protected abstract fun changeProtectedValueParameters()
override fun onSaveInstanceState(): Parcelable? {
onSaveInstanceState?.invoke()
return ProtectionState(super.onSaveInstanceState()).apply {
this.isCurrentlyProtected = isCurrentlyProtected()
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
when (state) {
is ProtectionState -> {
super.onRestoreInstanceState(state.superState)
mIsCurrentlyProtected = state.isCurrentlyProtected
}
else -> super.onRestoreInstanceState(state)
}
}
internal class ProtectionState : BaseSavedState {
var isCurrentlyProtected: Boolean = true
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
isCurrentlyProtected = parcel.readBooleanCompat()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeBooleanCompat(isCurrentlyProtected)
}
companion object CREATOR : Creator<ProtectionState> {
override fun createFromParcel(parcel: Parcel): ProtectionState {
return ProtectionState(parcel)
}
override fun newArray(size: Int): Array<ProtectionState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard
import com.kunzisoft.keepass.utils.readListCompat
import com.kunzisoft.keepass.utils.readParcelableCompat
@@ -45,11 +46,12 @@ abstract class TemplateAbstractView<
private var mTemplate: Template? = null
protected var mEntryInfo: EntryInfo? = null
// To keep unprotected views during orientation change
protected var mUnprotectedFields = mutableListOf<Field>()
private var mViewFields = mutableListOf<ViewField>()
protected var mFontInVisibility: Boolean = PreferencesUtil.fieldFontIsInVisibility(context)
protected var mHideProtectedValue: Boolean = PreferencesUtil.hideProtectedValue(context)
protected var headerContainerView: ViewGroup
protected var entryIconView: ImageView
protected var backgroundColorView: View
@@ -121,9 +123,6 @@ abstract class TemplateAbstractView<
}
private fun buildTemplate() {
// Retrieve preferences
mHideProtectedValue = PreferencesUtil.hideProtectedValue(context)
// Build each template section
titleContainerView.removeAllViews()
templateContainerView.removeAllViews()
@@ -284,10 +283,6 @@ abstract class TemplateAbstractView<
this.mFontInVisibility = fontInVisibility
}
fun setHideProtectedValue(hideProtectedValue: Boolean) {
this.mHideProtectedValue = hideProtectedValue
}
fun setEntryInfo(entryInfo: EntryInfo?) {
mEntryInfo = entryInfo
buildTemplateAndPopulateInfo()
@@ -578,7 +573,7 @@ abstract class TemplateAbstractView<
}
return if (!isStandardFieldName(customField.name)) {
customFieldsContainerView.visibility = View.VISIBLE
customFieldsContainerView.visibility = VISIBLE
if (getIndexViewFieldByName(customField.name) >= 0) {
// Update a custom field with a new value,
// new field name must be the same as old field name
@@ -683,6 +678,16 @@ abstract class TemplateAbstractView<
putCustomField(Field(otpField.name, otpField.protectedValue))
}
fun saveUnprotectedFieldState(field: Field, isCurrentlyProtected: Boolean) {
try {
if (!isCurrentlyProtected) {
mUnprotectedFields.add(field)
} else {
mUnprotectedFields.remove(field)
}
} catch (_: Exception) {}
}
override fun onRestoreInstanceState(state: Parcelable?) {
//begin boilerplate code so parent classes can restore state
if (state !is SavedState) {
@@ -691,6 +696,7 @@ abstract class TemplateAbstractView<
} else {
mTemplate = state.template
mEntryInfo = state.entryInfo
mUnprotectedFields = state.unprotectedFields
onRestoreEntryInstanceState(state)
buildTemplateAndPopulateInfo()
super.onRestoreInstanceState(state.superState)
@@ -706,6 +712,7 @@ abstract class TemplateAbstractView<
retrieveDefaultValues = false)
saveState.template = this.mTemplate
saveState.entryInfo = this.mEntryInfo
saveState.unprotectedFields = this.mUnprotectedFields
onSaveEntryInstanceState(saveState)
return saveState
}
@@ -715,6 +722,7 @@ abstract class TemplateAbstractView<
protected class SavedState : BaseSavedState {
var template: Template? = null
var entryInfo: EntryInfo? = null
var unprotectedFields = mutableListOf<Field>()
// TODO Move
var tempDateTimeViewId: Int? = null
@@ -723,6 +731,7 @@ abstract class TemplateAbstractView<
private constructor(parcel: Parcel) : super(parcel) {
template = parcel.readParcelableCompat() ?: template
entryInfo = parcel.readParcelableCompat() ?: entryInfo
parcel.readListCompat<Field>(unprotectedFields)
val dateTimeViewId = parcel.readInt()
if (dateTimeViewId != -1)
tempDateTimeViewId = dateTimeViewId
@@ -732,6 +741,7 @@ abstract class TemplateAbstractView<
super.writeToParcel(out, flags)
out.writeParcelable(template, flags)
out.writeParcelable(entryInfo, flags)
out.writeList(unprotectedFields)
out.writeInt(tempDateTimeViewId ?: -1)
}

View File

@@ -18,9 +18,9 @@ import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.AppOriginEntryField
import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.AppOriginEntryField
import com.kunzisoft.keepass.model.PasskeyEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -35,6 +35,11 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
@IdRes
private var mTempDateTimeViewId: Int? = null
private var mOnUnprotectClickListener: ((Field, ProtectedFieldView) -> Unit)? = null
fun setOnUnprotectClickListener(listener: ((Field, ProtectedFieldView) -> Unit)?) {
this.mOnUnprotectClickListener = listener
}
private var mOnCustomEditionActionClickListener: ((Field) -> Unit)? = null
fun setOnCustomEditionActionClickListener(listener: ((Field) -> Unit)?) {
this.mOnCustomEditionActionClickListener = listener
@@ -80,9 +85,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
if (color != null) {
backgroundColorView.background.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
backgroundColorView.visibility = View.VISIBLE
backgroundColorView.visibility = VISIBLE
} else {
backgroundColorView.visibility = View.GONE
backgroundColorView.visibility = GONE
}
}
@@ -103,9 +108,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
if (color != null) {
foregroundColorView.background.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
foregroundColorView.visibility = View.VISIBLE
foregroundColorView.visibility = VISIBLE
} else {
foregroundColorView.visibility = View.GONE
foregroundColorView.visibility = GONE
}
}
@@ -113,14 +118,25 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
headerContainerView.isVisible = true
}
override fun buildLinearTextView(templateAttribute: TemplateAttribute,
field: Field): TextEditFieldView? {
override fun buildLinearTextView(
templateAttribute: TemplateAttribute,
field: Field
): TextEditFieldView? {
return context?.let {
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextEditFieldView(it)
else TextEditFieldView(it)).apply {
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
setProtection(field.protectedValue.isProtected)
setProtection(
protection = field.protectedValue.isProtected,
isCurrentlyProtected = mUnprotectedFields.contains(field).not()
) {
mOnUnprotectClickListener?.invoke(field, this)
}
// Trick to bypass the onSaveInstanceState in rebuild child
onSaveInstanceState = {
saveUnprotectedFieldState(field, isCurrentlyProtected())
}
default = templateAttribute.default
setMaxChars(templateAttribute.options.getNumberChars())
setMaxLines(templateAttribute.options.getNumberLines())
@@ -129,7 +145,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
textDirection = TEXT_DIRECTION_LTR
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
}
}
}
@@ -143,7 +159,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
default = templateAttribute.default
setActionClick(templateAttribute, field, this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
}
}
}
@@ -157,7 +173,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
label = templateAttribute.alias
?: TemplateField.getLocalizedName(context, field.name)
val fieldValue = field.protectedValue.stringValue
value = if (fieldValue.isEmpty()) templateAttribute.default else fieldValue
value = fieldValue.ifEmpty { templateAttribute.default }
// TODO edition and password generator at same time
when (templateAttribute.action) {
TemplateAttributeAction.NONE -> {
@@ -187,7 +203,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
val value = field.protectedValue.toString().trim()
type = dateInstantType
activation = value.isNotEmpty()
} catch (e: Exception) {
} catch (_: Exception) {
type = dateInstantType
activation = false
}

View File

@@ -25,12 +25,17 @@ class TemplateView @JvmOverloads constructor(context: Context,
: TemplateAbstractView<TextFieldView, TextFieldView, DateTimeFieldView>
(context, attrs, defStyle) {
private var mOnUnprotectClickListener: ((ProtectedFieldView) -> Unit)? = null
fun setOnUnprotectClickListener(listener: ((ProtectedFieldView) -> Unit)?) {
this.mOnUnprotectClickListener = listener
}
private var mOnAskCopySafeClickListener: (() -> Unit)? = null
fun setOnAskCopySafeClickListener(listener: (() -> Unit)? = null) {
this.mOnAskCopySafeClickListener = listener
}
private var mOnCopyActionClickListener: ((Field) -> Unit)? = null
fun setOnCopyActionClickListener(listener: ((Field) -> Unit)? = null) {
private var mOnCopyActionClickListener: ((Field, ProtectedFieldView) -> Unit)? = null
fun setOnCopyActionClickListener(listener: ((Field, ProtectedFieldView) -> Unit)? = null) {
this.mOnCopyActionClickListener = listener
}
@@ -58,7 +63,16 @@ class TemplateView @JvmOverloads constructor(context: Context,
PasskeyTextFieldView(it)
else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
setProtection(
protection = field.protectedValue.isProtected,
isCurrentlyProtected = mUnprotectedFields.contains(field).not()
) {
mOnUnprotectClickListener?.invoke(this)
}
// Trick to bypass the onSaveInstanceState in rebuild child
onSaveInstanceState = {
saveUnprotectedFieldState(field, isCurrentlyProtected())
}
label = templateAttribute.alias
?: TemplateField.getLocalizedName(context, field.name)
setMaxChars(templateAttribute.options.getNumberChars())
@@ -78,7 +92,15 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(true, value)))
?.invoke(
Field(
name = label,
value = ProtectedString(
enableProtection = true,
string = value
)
), this
)
}
} else {
setCopyButtonState(TextFieldView.ButtonState.GONE)
@@ -89,7 +111,15 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(false, value)))
?.invoke(
Field(
name = label,
value = ProtectedString(
enableProtection = false,
string = value
)
), this
)
}
}
}
@@ -114,7 +144,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
try {
val value = field.protectedValue.toString().trim()
activation = value.isNotEmpty()
} catch (e: Exception) {
} catch (_: Exception) {
activation = false
}
}
@@ -177,9 +207,15 @@ class TemplateView @JvmOverloads constructor(context: Context,
value = otpElement.tokenString
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { _, _ ->
mOnCopyActionClickListener?.invoke(Field(
otpElement.type.name,
ProtectedString(false, otpElement.token)))
mOnCopyActionClickListener?.invoke(
Field(
name = otpElement.type.name,
value = ProtectedString(
enableProtection = false,
string = otpElement.token
)
), this
)
}
textDirection = TEXT_DIRECTION_LTR
mLastOtpTokenView = this

View File

@@ -6,12 +6,13 @@ import android.text.InputFilter
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.text.method.PasswordTransformationMethod
import android.text.method.SingleLineTransformationMethod
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.annotation.DrawableRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.AppCompatImageButton
@@ -21,12 +22,11 @@ import androidx.core.view.isVisible
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
open class TextEditFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
: ProtectedTextFieldView(context, attrs, defStyle) {
private var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId()
@@ -169,14 +169,29 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
}
}
fun setProtection(protection: Boolean) {
if (protection) {
override fun setProtection(
protection: Boolean,
isCurrentlyProtected: Boolean,
onUnprotectClickListener: OnClickListener?
) {
super.setProtection(protection, isCurrentlyProtected, onUnprotectClickListener)
if (isProtected) {
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
val visibilityTag = if (PreferencesUtil.hideProtectedValue(context))
InputType.TYPE_TEXT_VARIATION_PASSWORD
else
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
valueView.inputType = valueView.inputType or visibilityTag
// FIXME Called by itself during orientation change
/*
labelView.setEndIconOnClickListener {
onUnprotectClickListener?.onClick(this@TextEditFieldView)
}*/
}
}
override fun changeProtectedValueParameters() {
if (isCurrentlyProtected()) {
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
valueView.transformationMethod = PasswordTransformationMethod.getInstance()
} else {
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
valueView.transformationMethod = SingleLineTransformationMethod.getInstance()
}
}

View File

@@ -26,7 +26,6 @@ import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
import android.widget.RelativeLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton
import androidx.appcompat.widget.AppCompatTextView
@@ -42,7 +41,7 @@ import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
open class TextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
: ProtectedTextFieldView(context, attrs, defStyle) {
protected var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId()
@@ -204,25 +203,29 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
}
}
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) {
showButton.isVisible = protection
showButton.isSelected = hiddenProtectedValue
showButton.setOnClickListener {
showButton.isSelected = !showButton.isSelected
changeProtectedValueParameters()
override fun setProtection(
protection: Boolean,
isCurrentlyProtected: Boolean,
onUnprotectClickListener: OnClickListener?
) {
super.setProtection(protection, isCurrentlyProtected, onUnprotectClickListener)
showButton.isVisible = isProtected
if (isProtected) {
showButton.setOnClickListener {
onUnprotectClickListener?.onClick(this@TextFieldView)
}
}
changeProtectedValueParameters()
invalidate()
}
protected fun changeProtectedValueParameters() {
override fun changeProtectedValueParameters() {
valueView.apply {
if (showButton.isVisible) {
applyHiddenStyle(showButton.isSelected)
applyHiddenStyle(isCurrentlyProtected())
} else {
linkify()
}
}
invalidate()
}
private fun linkify() {

View File

@@ -238,15 +238,13 @@ fun View.updateLockPaddingStart() {
}
}
fun Context.toastError(e: Throwable) {
Toast.makeText(
applicationContext,
if (e is LocalizedException)
e.getLocalizedMessage(resources)
else
e.localizedMessage,
Toast.LENGTH_LONG
).show()
fun Context.toastError(e: Throwable?) {
val message = if (e is LocalizedException)
e.getLocalizedMessage(resources)
else e?.localizedMessage
message?.let {
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
}
}
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
@@ -259,6 +257,15 @@ fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
}
}
fun CoordinatorLayout.showError(error: Throwable?) {
val message = if (error is LocalizedException) {
error.getLocalizedMessage(resources) ?: error.message
} else error?.message
message?.let {
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
}
}
fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->

View File

@@ -20,6 +20,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.view.ProtectedFieldView
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
@@ -37,7 +38,6 @@ class EntryEditViewModel: NodeEditViewModel() {
// To show dialog only one time
var backPressedAlreadyApproved = false
var warningOverwriteDataAlreadyApproved = false
// Useful to not relaunch a current action
private var actionLocked: Boolean = false
@@ -81,8 +81,8 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
private val mEntryEditState = MutableStateFlow<EntryEditState>(EntryEditState.Loading)
val entryEditState: StateFlow<EntryEditState> = mEntryEditState
fun loadTemplateEntry(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
@@ -125,7 +125,7 @@ class EntryEditViewModel: NodeEditViewModel() {
mEntryId = null
_templatesEntry.value = templatesEntry
if (templatesEntry?.overwrittenData == true) {
mUiState.value = UIState.ShowOverwriteMessage
mEntryEditState.value = EntryEditState.ShowOverwriteMessage
}
}
).execute()
@@ -293,6 +293,10 @@ class EntryEditViewModel: NodeEditViewModel() {
_onPasswordSelected.value = passwordField
}
fun requestUnprotectField(fieldView: ProtectedFieldView) {
mEntryEditState.value = EntryEditState.RequestUnprotectField(fieldView)
}
fun requestCustomFieldEdition(customField: Field) {
_requestCustomFieldEdition.value = customField
}
@@ -348,6 +352,10 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
}
fun actionPerformed() {
mEntryEditState.value = EntryEditState.Loading
}
data class TemplatesEntry(
val isTemplate: Boolean,
val templates: List<Template>,
@@ -362,9 +370,12 @@ class EntryEditViewModel: NodeEditViewModel() {
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState {
object Loading: UIState()
object ShowOverwriteMessage: UIState()
sealed class EntryEditState {
object Loading: EntryEditState()
object ShowOverwriteMessage: EntryEditState()
data class RequestUnprotectField(
val protectedFieldView: ProtectedFieldView
): EntryEditState()
}
companion object {

View File

@@ -19,25 +19,43 @@
*/
package com.kunzisoft.keepass.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.view.ProtectedFieldView
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
class EntryViewModel: ViewModel() {
class EntryViewModel(application: Application): AndroidViewModel(application) {
private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
var mainEntryId: NodeId<UUID>? = null
private set
var entryInfo: EntryInfo? = null
private set
var historyPosition: Int = -1
private set
var entryIsHistory: Boolean = false
private set
var entryLoaded = false
private set
private var mClipboardHelper: ClipboardHelper = ClipboardHelper(application)
val entryInfoHistory : LiveData<EntryInfoHistory?> get() = _entryInfoHistory
private val _entryInfoHistory = MutableLiveData<EntryInfoHistory?>()
@@ -59,13 +77,16 @@ class EntryViewModel: ViewModel() {
val historySelected : LiveData<EntryHistory> get() = _historySelected
private val _historySelected = SingleLiveEvent<EntryHistory>()
private val mEntryState = MutableStateFlow<EntryState>(EntryState.Loading)
val entryState: StateFlow<EntryState> = mEntryState
fun loadDatabase(database: ContextualDatabase?) {
loadEntry(database, mMainEntryId, mHistoryPosition)
loadEntry(database, mainEntryId, historyPosition)
}
fun loadEntry(database: ContextualDatabase?, mainEntryId: NodeId<UUID>?, historyPosition: Int = -1) {
this.mMainEntryId = mainEntryId
this.mHistoryPosition = historyPosition
this.mainEntryId = mainEntryId
this.historyPosition = historyPosition
if (database != null && mainEntryId != null) {
IOActionTask(
@@ -104,6 +125,13 @@ class EntryViewModel: ViewModel() {
}
},
{ entryInfoHistory ->
if (entryInfoHistory != null) {
this.mainEntryId = entryInfoHistory.mainEntryId
this.entryInfo = entryInfoHistory.entryInfo
this.historyPosition = historyPosition
this.entryIsHistory = historyPosition > -1
this.entryLoaded = true
}
_entryInfoHistory.value = entryInfoHistory
_entryHistory.value = entryInfoHistory?.entryHistory
}
@@ -111,6 +139,18 @@ class EntryViewModel: ViewModel() {
}
}
fun requestUnprotectField(fieldView: ProtectedFieldView) {
mEntryState.value = EntryState.RequestUnprotectField(fieldView)
}
fun requestCopyField(field: Field, fieldView: ProtectedFieldView) {
// Only request the User Verification if the field is protected and not shown
if (field.protectedValue.isProtected && fieldView.isCurrentlyProtected())
mEntryState.value = EntryState.RequestCopyProtectedField(field)
else
copyToClipboard(field)
}
fun onOtpElementUpdated(optElement: OtpElement?) {
_onOtpElementUpdated.value = optElement
}
@@ -131,6 +171,22 @@ class EntryViewModel: ViewModel() {
_sectionSelected.value = section
}
fun copyToClipboard(field: Field) {
mClipboardHelper.timeoutCopyToClipboard(
TemplateField.getLocalizedName(getApplication(), field.name),
field.protectedValue.stringValue,
field.protectedValue.isProtected
)
}
fun copyToClipboard(text: String) {
mClipboardHelper.timeoutCopyToClipboard(text, text)
}
fun actionPerformed() {
mEntryState.value = EntryState.Loading
}
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
var historyPosition: Int,
val template: Template,
@@ -152,6 +208,16 @@ class EntryViewModel: ViewModel() {
}
}
sealed class EntryState {
object Loading: EntryState()
data class RequestUnprotectField(
val protectedFieldView: ProtectedFieldView
): EntryState()
data class RequestCopyProtectedField(
val field: Field
): EntryState()
}
companion object {
private val TAG = EntryViewModel::class.java.name
}

View File

@@ -0,0 +1,48 @@
package com.kunzisoft.keepass.viewmodels
import android.net.Uri
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.MainCredential
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* ViewModel for the Main Credential Dialog
* Easily retrieves main credential from the database identified by its URI
*/
class MainCredentialViewModel: ViewModel() {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun validateMainCredential(
databaseUri: Uri,
mainCredential: MainCredential
) {
mUiState.value = UIState.OnMainCredentialEntered(databaseUri, mainCredential)
}
fun cancelMainCredential(
databaseUri: Uri?,
error: Throwable? = null
) {
mUiState.value = UIState.OnMainCredentialCanceled(databaseUri, error)
}
fun onActionReceived() {
mUiState.value = UIState.Loading
}
sealed class UIState {
object Loading: UIState()
data class OnMainCredentialEntered(
val databaseUri: Uri,
val mainCredential: MainCredential
): UIState()
data class OnMainCredentialCanceled(
val databaseUri: Uri?,
val error: Throwable?
): UIState()
}
}

View File

@@ -0,0 +1,31 @@
package com.kunzisoft.keepass.viewmodels
import android.app.Application
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class SettingsViewModel(application: Application): AndroidViewModel(application) {
private val mSettingsState = MutableStateFlow<SettingsState>(SettingsState.Wait)
val settingsState: StateFlow<SettingsState> = mSettingsState
var dialogFragment: DialogFragment? = null
fun showError(error: Throwable?) {
mSettingsState.value = SettingsState.ShowError(error)
}
fun errorShown() {
mSettingsState.value = SettingsState.Wait
}
sealed class SettingsState {
object Wait: SettingsState()
data class ShowError(
val error: Throwable? = null
): SettingsState()
}
}

View File

@@ -0,0 +1,58 @@
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.database.element.MasterCredential.CREATOR.getCheckKey
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* ViewModel for the User Verification
*/
class UserVerificationViewModel: ViewModel() {
private val mUVState = MutableStateFlow<UVState>(UVState.Loading)
val userVerificationState: StateFlow<UVState> = mUVState
var dataToVerify: UserVerificationData? = null
fun checkMainCredential(checkString: String) {
// Check the password part
val data = dataToVerify
if (data?.database?.checkKey(getCheckKey(checkString)) == true)
onUserVerificationSucceeded(data)
else {
onUserVerificationFailed(dataToVerify, InvalidCredentialsDatabaseException())
}
dataToVerify = null
}
fun onUserVerificationSucceeded(dataToVerify: UserVerificationData) {
mUVState.value = UVState.OnUserVerificationSucceeded(dataToVerify)
}
fun onUserVerificationFailed(
dataToVerify: UserVerificationData? = null,
error: Throwable? = null
) {
this.dataToVerify = dataToVerify
mUVState.value = UVState.OnUserVerificationCanceled(dataToVerify, error)
}
fun onUserVerificationReceived() {
mUVState.value = UVState.Loading
}
sealed class UVState {
object Loading: UVState()
data class OnUserVerificationSucceeded(
val dataToVerify: UserVerificationData
): UVState()
data class OnUserVerificationCanceled(
val dataToVerify: UserVerificationData?,
val error: Throwable?
): UVState()
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11,4C9.95,4 9.063,4.363 8.338,5.088C7.613,5.813 7.25,6.7 7.25,7.75C7.25,8.8 7.613,9.687 8.338,10.412C9.063,11.137 9.95,11.5 11,11.5C12.05,11.5 12.937,11.137 13.662,10.412C14.387,9.687 14.75,8.8 14.75,7.75C14.75,6.7 14.387,5.813 13.662,5.088C12.937,4.363 12.05,4 11,4zM11,13.5C10.567,13.5 10.147,13.521 9.738,13.563C9.33,13.604 8.908,13.667 8.475,13.75C8.47,13.754 8.049,13.845 7.912,13.875C6.887,14.125 5.816,14.5 4.699,15C4.199,15.233 3.791,15.575 3.475,16.025C3.158,16.475 3,17.017 3,17.65L3,20L6,20L13.682,20C13.429,19.394 13.287,18.729 13.287,18.031C13.287,16.501 13.971,15.144 15.033,14.205C14.724,14.11 14.389,13.949 14.088,13.875C14.08,13.877 13.525,13.75 13.525,13.75C13.301,13.707 13.089,13.684 12.871,13.652C12.721,13.63 12.57,13.603 12.42,13.586C12.368,13.58 12.313,13.568 12.262,13.563C11.853,13.521 11.433,13.5 11,13.5zM18.414,14.445C16.433,14.445 14.826,16.05 14.826,18.031C14.826,20.012 16.433,21.619 18.414,21.619C20.395,21.619 22,20.012 22,18.031C22,16.05 20.395,14.445 18.414,14.445zM19.848,16.314C19.956,16.314 20.064,16.355 20.146,16.438L20.443,16.736C20.608,16.9 20.607,17.168 20.443,17.332L18.145,19.633C17.98,19.797 17.711,19.797 17.547,19.633L16.391,18.475C16.226,18.31 16.226,18.043 16.391,17.879L16.689,17.58C16.854,17.416 17.121,17.416 17.285,17.58L17.846,18.139L19.549,16.438C19.631,16.355 19.739,16.314 19.848,16.314z"
android:fillColor="#7D7D7D"/>
</vector>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2025 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:padding="@dimen/default_margin"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/user_verification_information"
android:text="@string/user_verification_required_title"
style="@style/KeepassDXStyle.Title"/>
<ImageView
android:id="@+id/user_verification_information"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_info_white_24dp"
style="@style/KeepassDXStyle.ImageButton.Simple"
android:contentDescription="@string/content_description_user_verification_information"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/user_verification_required_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin_horizontal"
android:layout_marginLeft="@dimen/card_view_margin_horizontal"
android:layout_marginEnd="@dimen/card_view_margin_horizontal"
android:layout_marginRight="@dimen/card_view_margin_horizontal"
android:text="@string/user_verification_database_credential"
android:textColor="?attr/colorSecondary"/>
<!-- Password -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_check_password_input_layout"
android:layout_margin="@dimen/card_view_margin_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorSecondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/setup_check_password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusedByDefault="true"
android:inputType="textPassword"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:hint="@string/first_chars" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>

View File

@@ -75,8 +75,7 @@
<com.kunzisoft.keepass.view.PasswordEditView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordVisible="false"/>
android:layout_height="wrap_content"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_repeat_input_layout"
android:layout_width="match_parent"

View File

@@ -61,7 +61,6 @@
<string name="list_size_summary">حجم النص في قائمة العناصر</string>
<string name="loading_database">يحمل قاعدة البيانات…</string>
<string name="lowercase">حروف صغيرة</string>
<string name="hide_password_summary">أخفِ كلمات السر (***) افتراضيًا</string>
<string name="about">عن التطبيق</string>
<string name="menu_change_key_settings">تغيير المفتاح الرئيسي</string>
<string name="settings">الإعدادات</string>
@@ -143,13 +142,11 @@
<string name="list_entries_show_username_summary">يعرض اسماء المستخدمين في قوائم المدخلات</string>
<string name="hint_generated_password">كلمة السر مولّدة</string>
<string name="hint_keyfile">ملف المفتاح</string>
<string name="hide_password_title">أخفِ كلمات السر</string>
<string name="copy_field">نُسخة من %1$s</string>
<string name="menu_copy">نسخ</string>
<string name="menu_move">نقل</string>
<string name="menu_paste">لصق</string>
<string name="menu_cancel">ألغِ</string>
<string name="menu_hide_password">أخفِ كلمة السر</string>
<string name="menu_showpass">أظهر كلمة السر</string>
<string name="menu_url">الانتقال الى الرابط</string>
<string name="menu_file_selection_read_only">محمي من التعديل</string>

View File

@@ -142,7 +142,6 @@
<string name="menu_paste">Mubadilə buferindən əlavə et</string>
<string name="menu_delete">Sil</string>
<string name="menu_cancel">Ləğv et</string>
<string name="menu_hide_password">Şifrəni gizlət</string>
<string name="menu_lock">Məlumat bazasını kilidlə</string>
<string name="menu_save_database">Məlumatları yadda saxla</string>
<string name="menu_merge_database">Məlumatları birləşdir</string>
@@ -448,8 +447,6 @@
<string name="invalid_db_sig">Məlumat bazasının formatını tanımaq mümkün olmadı.</string>
<string name="keyfile_is_empty">Açar faylı boşdur.</string>
<string name="length">Uzunluq</string>
<string name="hide_password_title">Şifrələri gizlət</string>
<string name="hide_password_summary">Şifrələri standart olaraq (***) ilə maskala</string>
<string name="colorize_password_title">Şifrələri rəngləndir</string>
<string name="colorize_password_summary">Şifrə hərflərini (simvollarını) növə görə rəngləndir</string>
<string name="list_entries_show_username_title">İstifadəçi adlarını göstər</string>

View File

@@ -29,7 +29,6 @@
<string name="menu_open">Otvori</string>
<string name="menu_save_database">Sačuvaj podatake</string>
<string name="menu_lock">Zaključaj bazu podataka</string>
<string name="menu_hide_password">Sakrij lozinku</string>
<string name="menu_cancel">Otkaži</string>
<string name="menu_delete">Izbriši</string>
<string name="menu_paste">Nalepi</string>
@@ -47,8 +46,6 @@
<string name="copy_field">Kopija od %1$s</string>
<string name="menu_change_key_settings">Promeni glavni ključ</string>
<string name="about">O aplikaciji</string>
<string name="hide_password_summary">Podrazumevaj maskiranje lozinki sa (***)</string>
<string name="hide_password_title">Sakrij lozinke</string>
<string name="lowercase">Mala slova</string>
<string name="loading_database">Učitavanje baze podataka…</string>
<string name="creating_database">Kreiranje baze podataka…</string>

View File

@@ -216,8 +216,6 @@
<string name="keyfile_is_empty">Файл ключа пусты.</string>
<string name="length">Даўжыня</string>
<string name="nodes">Вузлы</string>
<string name="hide_password_title">Схаваць паролі</string>
<string name="hide_password_summary">Маскіраваць паролі (***) па змаўчанні</string>
<string name="colorize_password_title">Размаляваць паролі</string>
<string name="colorize_password_summary">Размаляваць сімвалы пароля па тыпу</string>
<string name="list_entries_show_username_title">Паказаць імёны карыстальнікаў</string>
@@ -258,7 +256,6 @@
<string name="menu_paste">Уставіць</string>
<string name="menu_delete">Выдаліць</string>
<string name="menu_cancel">Адмена</string>
<string name="menu_hide_password">Схаваць пароль</string>
<string name="menu_lock">Заблакаваць базу дадзеных</string>
<string name="menu_save_database">Захаваць дадзеныя</string>
<string name="menu_merge_database">Аб\'яднаць дадзеныя</string>

View File

@@ -71,10 +71,8 @@
<string name="unlock">Отключване</string>
<string name="unavailable_feature_hardware">Необходимият хардуер не може да бъде намерен.</string>
<string name="hardware_key">Хардуерен ключ</string>
<string name="hide_password_summary">Скриване на паролите (***) по подразбиране</string>
<string name="select_database_file">Отключване на хранилище</string>
<string name="content_description_hardware_key_checkbox">Отметка на поле с хардуерен ключ</string>
<string name="hide_password_title">Скриване на пароли</string>
<string name="hint_pass">Парола</string>
<string name="education_select_database_title">Отворете съществуващо хранилище</string>
<string name="content_description_keyfile_checkbox">Отметка на поле за файл с ключ</string>
@@ -157,7 +155,6 @@
<string name="template">Шаблон</string>
<string name="menu_move">Преместване</string>
<string name="menu_cancel">Отказ</string>
<string name="menu_hide_password">Скриване на парола</string>
<string name="auto_focus_search_summary">Търсене при отключване на хранилище</string>
<string name="saving_database">Запазване на хранилището…</string>
<string name="command_execution">Изпълнение на команда…</string>

View File

@@ -182,7 +182,6 @@
<string name="invalid_db_same_uuid">একই UUID সহ %1$s %2$s ইতিমধ্যেই বিদ্যমান।</string>
<string name="passphrase">পাসফ্রেজ</string>
<string name="keyfile_is_empty">কী ফাইলটি খালি।</string>
<string name="hide_password_title">পাসওয়ার্ড লুকান</string>
<string name="list_size_summary">উপাদান তালিকায় পাঠ্যের আকার</string>
<string name="creating_database">ডাটাবেস তৈরি করা হচ্ছে…</string>
<string name="loading_database">ডাটাবেস লোড হচ্ছে…</string>
@@ -316,7 +315,6 @@
<string name="field_value">ক্ষেত্রের মান</string>
<string name="corrupted_file">দূষিত ফাইল।</string>
<string name="show_uuid_title">UUID দেখান</string>
<string name="hide_password_summary">ডিফল্টরূপে মাস্ক পাসওয়ার্ড (***)</string>
<string name="list_entries_show_username_title">ব্যবহারকারীর নাম দেখান</string>
<string name="show_uuid_summary">একটি এন্ট্রি বা একটি গ্রুপের সাথে সংযুক্ত UUID প্রদর্শন করে</string>
<string name="menu_reload_database">ডেটা পুনরায় লোড করুন</string>
@@ -330,7 +328,6 @@
<string name="menu_merge_database">ডেটা মার্জ করুন</string>
<string name="menu_merge_from">থেকে মার্জ করুন…</string>
<string name="menu_showpass">পাসওয়ার্ড দেখাও</string>
<string name="menu_hide_password">পাসওয়ার্ড লুকান</string>
<string name="menu_lock">ডাটাবেস লক করুন</string>
<string name="menu_url">URL-এ যান</string>
<string name="menu_empty_recycle_bin">রিসাইকেল বিন খালি করুন</string>

View File

@@ -84,8 +84,6 @@
<string name="list_size_summary">Mida del text a la llista de grups</string>
<string name="loading_database">Carregant base de dades…</string>
<string name="lowercase">Minúscules</string>
<string name="hide_password_title">Emmascara contrasenya</string>
<string name="hide_password_summary">Amaga les contrasenyes per defecte</string>
<string name="about">Sobre</string>
<string name="menu_change_key_settings">Canvia Clau Mestra</string>
<string name="settings">Paràmetres</string>
@@ -93,7 +91,6 @@
<string name="menu_delete">Esborra</string>
<string name="menu_donate">Donar</string>
<string name="menu_edit">Editar</string>
<string name="menu_hide_password">Amaga contrasenya</string>
<string name="menu_lock">Bloca la base de dades</string>
<string name="menu_open">Obre</string>
<string name="menu_search">Cerca</string>

View File

@@ -90,8 +90,6 @@
<string name="list_size_summary">Velikost textu v seznamu prvků</string>
<string name="loading_database">Načítám databázi…</string>
<string name="lowercase">Malá písmena</string>
<string name="hide_password_title">Skrýt hesla</string>
<string name="hide_password_summary">Ve výchozím stavu zobrazit (***) místo hesla</string>
<string name="about">O aplikaci</string>
<string name="menu_change_key_settings">Změnit hlavní klíč</string>
<string name="settings">Nastavení</string>
@@ -99,7 +97,6 @@
<string name="menu_delete">Smazat</string>
<string name="menu_donate">Přispět darem</string>
<string name="menu_edit">Upravit</string>
<string name="menu_hide_password">Skrýt heslo</string>
<string name="menu_lock">Zamknout databázi</string>
<string name="menu_open">Otevřít</string>
<string name="menu_search">Hledat</string>

View File

@@ -89,8 +89,6 @@
<string name="list_size_summary">Tekststørrelse i elementlisten</string>
<string name="loading_database">Indlæser database…</string>
<string name="lowercase">Små bogstaver</string>
<string name="hide_password_title">Skjul adgangskoder</string>
<string name="hide_password_summary">Skjul adgangskoder (***) som standard</string>
<string name="about">Om</string>
<string name="menu_change_key_settings">Skift hovednøgle</string>
<string name="settings">Indstillinger</string>
@@ -98,7 +96,6 @@
<string name="menu_delete">Slet</string>
<string name="menu_donate">Donér</string>
<string name="menu_edit">Rediger</string>
<string name="menu_hide_password">Skjul adgangskode</string>
<string name="menu_lock">Lås database</string>
<string name="menu_open">Åbn</string>
<string name="menu_search">Søg</string>

View File

@@ -99,8 +99,6 @@
<string name="list_size_summary">Schriftgröße der Listenelemente</string>
<string name="loading_database">Datenbank wird geladen </string>
<string name="lowercase">Kleinbuchstaben</string>
<string name="hide_password_title">Passwörter verbergen</string>
<string name="hide_password_summary">Passwörter mit (***) maskieren</string>
<string name="about">Über</string>
<string name="menu_change_key_settings">Hauptschlüssel ändern</string>
<string name="settings">Einstellungen</string>
@@ -108,7 +106,6 @@
<string name="menu_delete">Löschen</string>
<string name="menu_donate">Spenden</string>
<string name="menu_edit">Bearbeiten</string>
<string name="menu_hide_password">Passwort verbergen</string>
<string name="menu_lock">Datenbank sperren</string>
<string name="menu_open">Öffnen</string>
<string name="menu_search">Suche</string>

View File

@@ -92,8 +92,6 @@
<string name="list_size_summary">Μέγεθος κειμένου στη λίστα στοιχείων</string>
<string name="loading_database">Φόρτωση βάσης δεδομένων…</string>
<string name="lowercase">Μικρά</string>
<string name="hide_password_title">Απόκρυψη κωδικών πρόσβασης</string>
<string name="hide_password_summary">Μάσκα κωδικούς πρόσβασης (***) από προεπιλογή</string>
<string name="about">Σχετικά με</string>
<string name="menu_change_key_settings">Αλλαγή Κύριου Κλειδιού</string>
<string name="settings">Ρυθμίσεις</string>
@@ -101,7 +99,6 @@
<string name="menu_delete">Διαγραφή</string>
<string name="menu_donate">Δωρεά</string>
<string name="menu_edit">Επεξεργασία</string>
<string name="menu_hide_password">Απόκρυψη κωδικού</string>
<string name="menu_lock">Κλείδωμα βάσης δεδομένων</string>
<string name="menu_open">Άνοιγμα</string>
<string name="menu_search">Αναζήτηση</string>

View File

@@ -219,8 +219,6 @@
<string name="invalid_algorithm">Wrong algorithm.</string>
<string name="invalid_db_same_uuid">%1$s with the same UUID %2$s already exists.</string>
<string name="keyfile_is_empty">The keyfile is empty.</string>
<string name="hide_password_title">Hide passwords</string>
<string name="list_entries_show_username_title">Show usernames</string>
<string name="error_otp_period">Period must be between %1$d and %2$d seconds.</string>
<string name="error_string_type">This text does not match the requested item.</string>
<string name="error_registration_read_only">Saving a new item is not allowed in a read-only database.</string>
@@ -231,7 +229,6 @@
<string name="error_file_to_big">The file you are trying to upload is too big.</string>
<string name="error_upload_file">An error occurred while uploading the file data.</string>
<string name="error_location_unknown">Database location is unknown, database action cannot be performed.</string>
<string name="hide_password_summary">Mask passwords (***) by default</string>
<string name="list_entries_show_username_summary">Displays usernames in entry lists</string>
<string name="list_groups_show_number_entries_title">Show number of entries</string>
<string name="show_otp_token_summary">Displays OTP tokens in the list of entries</string>

View File

@@ -41,7 +41,6 @@
<string name="menu_reload_database">Reŝargi datumbazon</string>
<string name="menu_save_database">Konservi datumbazon</string>
<string name="menu_lock">Ŝlosi datumbazon</string>
<string name="menu_hide_password">Kaŝi pasvorton</string>
<string name="menu_cancel">Nuligi</string>
<string name="menu_delete">Forigi</string>
<string name="menu_paste">Algui</string>
@@ -56,8 +55,6 @@
<string name="settings">Agordoj</string>
<string name="copy_field">Kopio de %1$s</string>
<string name="about">Pri</string>
<string name="hide_password_summary">Kaŝi pasvortojn sub (***) defaŭlte</string>
<string name="hide_password_title">Kaŝi pasvortojn</string>
<string name="loading_database">Datumbaza ŝarĝado…</string>
<string name="creating_database">Datumbaza kreado…</string>
<string name="list_entries_show_username_summary">Vidigi uzantnomojn en elementaj listoj</string>

View File

@@ -84,8 +84,6 @@
<string name="list_size_summary">Tamaño del texto en la lista de elementos</string>
<string name="loading_database">Cargando base de datos…</string>
<string name="lowercase">Minúsculas</string>
<string name="hide_password_title">Ocultar contraseñas</string>
<string name="hide_password_summary">Oculta contraseñas (***) por defecto</string>
<string name="about">Acerca de</string>
<string name="menu_change_key_settings">Cambiar contraseña maestra</string>
<string name="settings">Configuración</string>
@@ -93,7 +91,6 @@
<string name="menu_delete">Eliminar</string>
<string name="menu_donate">Donar</string>
<string name="menu_edit">Editar</string>
<string name="menu_hide_password">Ocultar contraseña</string>
<string name="menu_lock">Bloquear la base de datos</string>
<string name="menu_open">Abrir</string>
<string name="menu_search">Buscar</string>

View File

@@ -197,8 +197,6 @@
<string name="hint_keyfile">Võtmefail</string>
<string name="hint_length">Pikkus</string>
<string name="password">Salasõna</string>
<string name="hide_password_title">Peida salasõnad</string>
<string name="hide_password_summary">Vakimisi peida salasõnad (***) maski taha</string>
<string name="colorize_password_title">Värvi salasõnad</string>
<string name="error_registration_read_only">Uue kirje salvestamine pole võimalik andmebaasis, milles on vaid lugemisõigus.</string>
<string name="error_database_uri_null">Andmebaasi ühtset ressursiidentifikaatorit ei õnnestu laadida.</string>
@@ -213,7 +211,6 @@
<string name="list_groups_show_number_entries_title">Näita kirjete arvu</string>
<string name="show_uuid_title">Näita UUID\'d</string>
<string name="show_uuid_summary">Näita kirje või grupiga seotud UUID\'d</string>
<string name="menu_hide_password">Peida salasõna</string>
<string name="menu_showpass">Näita salasõna</string>
<string name="error_cancel_by_user">Katkestatud kasutaja poolt.</string>
<string name="error_driver_required">%1$s draiver on vajalik.</string>

View File

@@ -93,8 +93,6 @@
<string name="list_size_summary">Testuaren tamaina taldearen listan</string>
<string name="loading_database">Datubasea kargatzen…</string>
<string name="lowercase">minuskulak</string>
<string name="hide_password_title">Pasahitza estali</string>
<string name="hide_password_summary">Pasahitza estali modu lehenetsian</string>
<string name="about">Honi buruz</string>
<string name="menu_change_key_settings">Gako nagusia aldatu</string>
<string name="settings">Ezarpenak</string>
@@ -102,7 +100,6 @@
<string name="menu_delete">Ezabatu</string>
<string name="menu_donate">Dirua eman</string>
<string name="menu_edit">Editatu</string>
<string name="menu_hide_password">Pasahitza ezkutatu</string>
<string name="menu_lock">Datu-basea blokeatu</string>
<string name="menu_open">Ireki</string>
<string name="menu_search">Bilatu</string>

View File

@@ -79,7 +79,6 @@
<string name="menu_open">باز</string>
<string name="menu_save_database">ذخیره پایگاه داده</string>
<string name="menu_lock">بانک اطلاعاتی قفل</string>
<string name="menu_hide_password">پنهان کردن رمز عبور</string>
<string name="menu_cancel">لغو</string>
<string name="menu_delete">حذف</string>
<string name="menu_paste">جاگذاری</string>
@@ -97,8 +96,6 @@
<string name="copy_field">کپی %1$s</string>
<string name="menu_change_key_settings">کلید اصلی را تغییر دهید</string>
<string name="about">در باره</string>
<string name="hide_password_summary">رمزهای عبور ماسک (***) به طور پیش فرض</string>
<string name="hide_password_title">پنهان کردن رمزهای عبور</string>
<string name="lowercase">ترجمه</string>
<string name="loading_database">پایگاه داده بارگذاری…</string>
<string name="creating_database">ایجاد پایگاه داده…</string>

View File

@@ -92,8 +92,6 @@
<string name="list_size_summary">Tekstin koko ryhmälistauksessa</string>
<string name="loading_database">Ladataan tietokantaa…</string>
<string name="lowercase">pienet kirjaimet</string>
<string name="hide_password_title">Piilota salasana</string>
<string name="hide_password_summary">Piilota salasanat oletuksena</string>
<string name="about">Tietoa</string>
<string name="menu_change_key_settings">Vaihda pääsalasanaa</string>
<string name="settings">Asetukset</string>
@@ -101,7 +99,6 @@
<string name="menu_delete">Poista</string>
<string name="menu_donate">Lahjoita</string>
<string name="menu_edit">Muokkaa</string>
<string name="menu_hide_password">Piilota salasana</string>
<string name="menu_lock">Lukitse tietokanta</string>
<string name="menu_open">Avaa</string>
<string name="menu_search">Etsi</string>

View File

@@ -218,8 +218,6 @@
<string name="invalid_db_sig">Hindi makilala ang format ng database.</string>
<string name="keyfile_is_empty">Walang laman ang keyfile.</string>
<string name="length">Haba</string>
<string name="hide_password_title">Itago ang mga password</string>
<string name="hide_password_summary">I-mask ang mga password (***) bilang default</string>
<string name="colorize_password_title">Kulayan ang mga password</string>
<string name="colorize_password_summary">Kulayan ang mga password character ayon sa uri</string>
<string name="list_entries_show_username_title">Ipakita ang mga username</string>
@@ -258,7 +256,6 @@
<string name="menu_paste">I-paste</string>
<string name="menu_delete">Burahin</string>
<string name="menu_cancel">Kanselahin</string>
<string name="menu_hide_password">Itago ang password</string>
<string name="menu_lock">I-lock ang database</string>
<string name="menu_save_database">I-save ang data</string>
<string name="menu_merge_database">I-merge ang data</string>

View File

@@ -98,8 +98,6 @@
<string name="list_size_summary">Taille du texte dans les éléments de liste</string>
<string name="loading_database">Chargement de la base de données…</string>
<string name="lowercase">Minuscules</string>
<string name="hide_password_title">Masquer les mots de passe</string>
<string name="hide_password_summary">Masque les mots de passe (***) par défaut</string>
<string name="about">À propos</string>
<string name="menu_change_key_settings">Modifier la clé principale</string>
<string name="copy_field">%1$s copié</string>
@@ -108,7 +106,6 @@
<string name="menu_delete">Supprimer</string>
<string name="menu_donate">Faire un don</string>
<string name="menu_edit">Modifier</string>
<string name="menu_hide_password">Masquer le mot de passe</string>
<string name="menu_lock">Verrouiller la base de données</string>
<string name="menu_open">Ouvrir</string>
<string name="menu_search">Rechercher</string>

View File

@@ -188,7 +188,6 @@
<string name="corrupted_file">Ficheiro corrompido.</string>
<string name="menu_keystore_remove_key">Borrar a clave do desbloqueo avanzado</string>
<string name="loading_database">A carregar base de datos…</string>
<string name="hide_password_summary">Mascarar contrasinais (***) por predefinición</string>
<string name="list_size_summary">Tamaño do texto na lista de elementos</string>
<string name="creating_database">A crear a base de datos…</string>
<string name="menu_move">Mover</string>
@@ -209,7 +208,6 @@
<string name="colorize_password_title">Colorear contrasinais</string>
<string name="colorize_password_summary">Colorear caracteres dos contrasinais por tipo</string>
<string name="settings">Configuracións</string>
<string name="menu_hide_password">Ocultar contrasinal</string>
<string name="menu_lock">Bloquear base de datos</string>
<string name="menu_save_database">Gardar datos</string>
<string name="no_results">Sen resultados da procura</string>
@@ -278,7 +276,6 @@
<string name="list_entries_show_username_title">Mostrar nomes de usuario</string>
<string name="hint_keyfile">Ficheiro clave</string>
<string name="hint_pass">Contrasinal</string>
<string name="hide_password_title">Ocultar contrasinais</string>
<string name="lowercase">Minúsculas</string>
<string name="about">Sobre</string>
<string name="copy_field">Copia de %1$s</string>

View File

@@ -113,7 +113,6 @@
<string name="list_groups_show_number_entries_summary">Prikazuje broj unosa u grupi</string>
<string name="creating_database">Stvaranje baze podataka …</string>
<string name="loading_database">Učitavanje baze podataka …</string>
<string name="hide_password_title">Sakrij lozinke</string>
<string name="menu_change_key_settings">Promjeni glavni ključ</string>
<string name="settings">Postavke</string>
<string name="menu_app_settings">Postavke aplikacije</string>
@@ -127,7 +126,6 @@
<string name="menu_copy">Kopiraj</string>
<string name="menu_paste">Umetni</string>
<string name="menu_delete">Izbriši</string>
<string name="menu_hide_password">Sakrij lozinku</string>
<string name="menu_lock">Zaključaj bazu podataka</string>
<string name="menu_save_database">Spremi podatke</string>
<string name="menu_open">Otvori</string>
@@ -249,7 +247,6 @@
<string name="list_size_title">Veličina elemenata popisa</string>
<string name="list_size_summary">Veličina teksta u popisu elemenata</string>
<string name="lowercase">Mala slova</string>
<string name="hide_password_summary">Standardno sakrij lozinke (***)</string>
<string name="about">O aplikaciji</string>
<string name="copy_field">Kopija od %1$s</string>
<string name="menu_move">Premjesti</string>

View File

@@ -92,8 +92,6 @@
<string name="list_size_summary">Szövegméret az elemlistában</string>
<string name="loading_database">Adatbázis betöltése…</string>
<string name="lowercase">Kisbetűk</string>
<string name="hide_password_title">Jelszavak elrejtése</string>
<string name="hide_password_summary">Jelszavak alapértelmezett elrejtése (***)</string>
<string name="about">Névjegy</string>
<string name="menu_change_key_settings">Mesterkulcs cseréje</string>
<string name="settings">Beállítások</string>
@@ -101,7 +99,6 @@
<string name="menu_delete">Törlés</string>
<string name="menu_donate">Támogatás</string>
<string name="menu_edit">Szerkesztés</string>
<string name="menu_hide_password">Jelszó elrejtése</string>
<string name="menu_lock">Adatbázis zárolása</string>
<string name="menu_open">Megnyitás</string>
<string name="menu_search">Keresés</string>

View File

@@ -14,7 +14,6 @@
<string name="menu_open">Buka</string>
<string name="menu_save_database">Simpan data</string>
<string name="menu_lock">Basis Data Terkunci</string>
<string name="menu_hide_password">Sembunyikan Kata Sandi</string>
<string name="menu_cancel">Batal</string>
<string name="menu_delete">Hapus</string>
<string name="menu_paste">Tempel</string>
@@ -32,8 +31,6 @@
<string name="copy_field">Salinan dari %1$s</string>
<string name="menu_change_key_settings">Ubah Kunci Utama</string>
<string name="about">Tentang</string>
<string name="hide_password_summary">Secara otomatis tutupi kata sandi (***)</string>
<string name="hide_password_title">Sembunyikan Kata Sandi</string>
<string name="lowercase">Huruf Kecil</string>
<string name="loading_database">Memuat basis data…</string>
<string name="creating_database">Pembuatan basis data…</string>

View File

@@ -94,8 +94,6 @@
<string name="list_size_summary">Dimensione del testo nell\'elenco del gruppo</string>
<string name="loading_database">Caricamento del database…</string>
<string name="lowercase">Minuscole</string>
<string name="hide_password_title">Nascondi le password</string>
<string name="hide_password_summary">Maschera le password (***) in modo predefinito</string>
<string name="about">Informazioni</string>
<string name="menu_change_key_settings">Modifica chiave principale</string>
<string name="settings">Impostazioni</string>
@@ -103,7 +101,6 @@
<string name="menu_delete">Elimina</string>
<string name="menu_donate">Dona</string>
<string name="menu_edit">Modifica</string>
<string name="menu_hide_password">Nascondi password</string>
<string name="menu_lock">Blocca database</string>
<string name="menu_open">Apri</string>
<string name="menu_search">Cerca</string>

View File

@@ -90,8 +90,6 @@
<string name="list_size_summary">גודל טקסט ברשימת הרכיבים</string>
<string name="loading_database">טוען מסד נתונים…</string>
<string name="lowercase">אות קטנה</string>
<string name="hide_password_title">הסתר סיסמאות</string>
<string name="hide_password_summary">הסווה סיסמאות (***) כברירת מחדל</string>
<string name="about">אודות</string>
<string name="menu_change_key_settings">שנה מפתח ראשי</string>
<string name="settings">העדפות</string>
@@ -99,7 +97,6 @@
<string name="menu_delete">מחק</string>
<string name="menu_donate">תרום</string>
<string name="menu_edit">ערוך</string>
<string name="menu_hide_password">הסתר סיסמה</string>
<string name="menu_lock">נעל מסד נתונים</string>
<string name="menu_open">פתח</string>
<string name="menu_search">חיפוש</string>

View File

@@ -159,8 +159,6 @@
<string name="creating_database">データベースを作成しています…</string>
<string name="loading_database">データベースを読み込んでいます…</string>
<string name="lowercase">小文字</string>
<string name="hide_password_title">パスワードを非表示にする</string>
<string name="hide_password_summary">デフォルトでパスワードを隠します(***と表示)</string>
<string name="about">概要</string>
<string name="menu_change_key_settings">マスターキーを変更</string>
<string name="copy_field">%1$s のコピー</string>
@@ -178,7 +176,6 @@
<string name="menu_paste">貼り付け</string>
<string name="menu_delete">削除</string>
<string name="menu_cancel">キャンセル</string>
<string name="menu_hide_password">パスワードを非表示にする</string>
<string name="menu_lock">データベースをロック</string>
<string name="menu_save_database">保存する</string>
<string name="menu_open">開く</string>

View File

@@ -100,8 +100,6 @@
<string name="list_size_summary">요소 목록 텍스트 크기</string>
<string name="loading_database">데이터베이스 로딩 중…</string>
<string name="lowercase">소문자</string>
<string name="hide_password_title">비밀번호 숨기기</string>
<string name="hide_password_summary">기본 비밀번호를 (***) 로 가리기</string>
<string name="about">정보</string>
<string name="menu_change_key_settings">마스터 키 바꾸기</string>
<string name="copy_field">%1$s의 사본</string>
@@ -116,7 +114,6 @@
<string name="menu_paste">붙여넣기</string>
<string name="menu_delete">삭제</string>
<string name="menu_cancel">취소</string>
<string name="menu_hide_password">비밀번호 숨기기</string>
<string name="menu_lock">데이터베이스 잠그기</string>
<string name="menu_open">열기</string>
<string name="menu_search">검색</string>

View File

@@ -81,7 +81,6 @@
<string name="entry_confpassword">Patvirtinti slaptažodį</string>
<string name="invalid_db_sig">Nepavyko atpažinti duomenų bazės formato.</string>
<string name="sort_db">Natūrali tvarka</string>
<string name="menu_hide_password">Slėpti slaptažodį</string>
<string name="app_timeout">Neveiklumas</string>
<string name="clipboard_error_clear">Nepavyko išvalyti iškarpinės</string>
<string name="list_size_summary">Teksto dydis grupės sąraše</string>
@@ -92,7 +91,6 @@
<string name="search">Įrašo pavadinimas/aprašymas</string>
<string name="menu_change_key_settings">Pakeisti pagrindinį raktą</string>
<string name="entry_accessed">Naudota</string>
<string name="hide_password_title">Maskuoti slaptažodį</string>
<string name="space">Tarpas</string>
<string name="special">Specialus</string>
<string name="uppercase">Didžiosios raidės</string>
@@ -100,7 +98,6 @@
<string name="minus">Minusas</string>
<string name="underline">Pabraukimas</string>
<string name="default_checkbox">Naudoti šią duomenų bazę kaip numatytąją</string>
<string name="hide_password_summary">Slėpti slaptažodžius pagal nutylėjimą</string>
<string name="invalid_algorithm">Neteisingas algoritmas.</string>
<string name="error_invalid_path">Pasirūpininkite, kad kelias būtų teisingas.</string>
<string name="education_unlock_summary">Įveskite slaptažodį ir/ar rakto failą, kad atrakintumėte savo duomenų bazę.

View File

@@ -89,8 +89,6 @@
<string name="list_size_summary">Teksta izmērs ierakstos un grupu sarakstos</string>
<string name="loading_database">Ielādēt datu bāzi…</string>
<string name="lowercase">Mazie burti</string>
<string name="hide_password_title">Sēpt paroles</string>
<string name="hide_password_summary">Sēpt paroles *****</string>
<string name="about">Par</string>
<string name="menu_change_key_settings">Mainīt galveno paroli</string>
<string name="settings">Iestatījumi</string>
@@ -98,7 +96,6 @@
<string name="menu_delete">Dzēst</string>
<string name="menu_donate">Ziedot</string>
<string name="menu_edit">Rediģēt</string>
<string name="menu_hide_password">Paslēpt paroli</string>
<string name="menu_lock">Bloķēt datu bāzi</string>
<string name="menu_open">Atvērt</string>
<string name="menu_search">Meklēšana</string>

View File

@@ -21,7 +21,6 @@
<string name="menu_search">തിരയുക</string>
<string name="menu_security_settings">സുരക്ഷാ ക്രമീകരണങ്ങൾ</string>
<string name="menu_database_settings">ഡാറ്റാബേസ് ക്രമീകരണങ്ങൾ</string>
<string name="hide_password_summary">സ്ഥിരസ്ഥിതിയായി പാസ്‌വേഡുകൾ (***) മാസ്ക് ചെയ്യുക</string>
<string name="invalid_algorithm">തെറ്റായ അൽഗോരിതം.</string>
<string name="password">പാസ്സ്‌വേഡ്</string>
<string name="hint_pass">പാസ്സ്‌വേഡ്</string>
@@ -52,7 +51,6 @@
<string name="menu_open">തുറക്കുക</string>
<string name="menu_save_database">ഡാറ്റാബേസ് സംരക്ഷിക്കുക</string>
<string name="menu_lock">ഡാറ്റാബേസ് ലോക്ക് ചെയ്യുക</string>
<string name="menu_hide_password">പാസ്‌വേഡുകൾ മറയ്‌ക്കുക</string>
<string name="menu_cancel">റദ്ദാക്കുക</string>
<string name="menu_delete">ഇല്ലാതാക്കുക</string>
<string name="menu_move">നീക്കുക</string>
@@ -62,7 +60,6 @@
<string name="menu_app_settings">അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ</string>
<string name="settings">ക്രമീകരണങ്ങൾ</string>
<string name="menu_change_key_settings">മാസ്റ്റർ കീ മാറ്റുക</string>
<string name="hide_password_title">പാസ്‌വേഡുകൾ മറയ്‌ക്കുക</string>
<string name="loading_database">ഡാറ്റാബേസ് ലോഡുചെയ്യുന്നു…</string>
<string name="creating_database">ഡാറ്റാബേസ് സൃഷ്ടിക്കുന്നു…</string>
<string name="list_entries_show_username_title">ഉപയോക്തൃനാമങ്ങൾ കാണിക്കുക</string>

View File

@@ -101,8 +101,6 @@
<string name="list_size_summary">Tekststørrelse i elemenetlisten</string>
<string name="loading_database">Laster database…</string>
<string name="lowercase">Små bokstaver</string>
<string name="hide_password_title">Masker passord</string>
<string name="hide_password_summary">Skjul passord som forvalg</string>
<string name="about">Om</string>
<string name="menu_change_key_settings">Endre hovednøkkel</string>
<string name="copy_field">Kopi av%1$s</string>
@@ -117,7 +115,6 @@
<string name="menu_paste">Lim inn</string>
<string name="menu_delete">Slett</string>
<string name="menu_cancel">Avbryt</string>
<string name="menu_hide_password">Skjul passord</string>
<string name="menu_lock">Lås database</string>
<string name="menu_open">Åpne</string>
<string name="menu_search">Søk</string>

View File

@@ -85,8 +85,6 @@
<string name="list_size_summary">Tekstgrootte in de itemslijst</string>
<string name="loading_database">Database laden…</string>
<string name="lowercase">Kleine letters</string>
<string name="hide_password_title">Wachtwoorden verbergen</string>
<string name="hide_password_summary">Wachtwoorden maskeren (***)</string>
<string name="about">Over</string>
<string name="menu_change_key_settings">Hoofdsleutel wijzigen</string>
<string name="settings">Instellingen</string>
@@ -94,7 +92,6 @@
<string name="menu_delete">Verwijderen</string>
<string name="menu_donate">Doneren</string>
<string name="menu_edit">Bewerken</string>
<string name="menu_hide_password">Wachtwoord verbergen</string>
<string name="menu_lock">Database vergrendelen</string>
<string name="menu_open">Openen</string>
<string name="menu_search">Zoeken</string>

View File

@@ -81,8 +81,6 @@
<string name="list_size_summary">Tekststorleik i gruppelista</string>
<string name="loading_database">Lastar databasen …</string>
<string name="lowercase">Små bokstavar</string>
<string name="hide_password_title">Skjul passord</string>
<string name="hide_password_summary">Skjul passord som forval</string>
<string name="about">Om</string>
<string name="menu_change_key_settings">Endra hovudnøkkelen</string>
<string name="settings">Innstillingar</string>
@@ -90,7 +88,6 @@
<string name="menu_delete">Slett</string>
<string name="menu_donate">Doner</string>
<string name="menu_edit">Endra</string>
<string name="menu_hide_password">Skjul passord</string>
<string name="menu_lock">Lås database</string>
<string name="menu_open">Opne</string>
<string name="menu_search">Søk</string>

View File

@@ -87,7 +87,6 @@
<string name="menu_open">ਖੋਲ੍ਹੋ</string>
<string name="menu_save_database">ਡਾਟਾਬੇਸ ਸੰਭਾਲੋ</string>
<string name="menu_lock">ਡਾਟਾਬੇਸ ਲਾਕ ਕਰੋ</string>
<string name="menu_hide_password">ਪਾਸਵਰਡ ਲੁਕਾਓ</string>
<string name="menu_cancel">ਰੱਦ ਕਰੋ</string>
<string name="menu_delete">ਹਟਾਓ</string>
<string name="menu_paste">ਚੇਪੋ</string>
@@ -105,8 +104,6 @@
<string name="copy_field">%1$s ਦੀ ਕਾਪੀ</string>
<string name="menu_change_key_settings">ਮਾਸਟਰ ਕੁੰਜੀ ਬਦਲੋ</string>
<string name="about">ਇਸ ਬਾਰੇ</string>
<string name="hide_password_summary">ਪਾਸਵਰਡਾਂ ਨੂੰ ਮੂਲ ਰੂਪ ਵਿੱਚ ਲੁਕਾਓ (***)</string>
<string name="hide_password_title">ਪਾਸਵਰਡ ਲੁਕਾਓ</string>
<string name="lowercase">ਛੋਟੇ ਅੱਖਰ (ਅੰਗਰੇਜ਼ੀ)</string>
<string name="loading_database">…ਡਾਟਾਬੇਸ ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
<string name="creating_database">…ਡਾਟਾਬੇਸ ਬਣਾਇਆ ਜਾ ਰਿਹਾ ਹੈ</string>

View File

@@ -81,8 +81,6 @@
<string name="list_size_summary">Rozmiar tekstu na liście elementów</string>
<string name="loading_database">Wczytywanie bazy danych…</string>
<string name="lowercase">Małe litery</string>
<string name="hide_password_title">Ukryj hasła</string>
<string name="hide_password_summary">Domyślnie maskuj hasła (***)</string>
<string name="about">O aplikacji</string>
<string name="menu_change_key_settings">Zmień klucz główny</string>
<string name="settings">Ustawienia</string>
@@ -90,7 +88,6 @@
<string name="menu_delete">Usuń</string>
<string name="menu_donate">Wspomóż</string>
<string name="menu_edit">Edytuj</string>
<string name="menu_hide_password">Ukryj hasło</string>
<string name="menu_lock">Zablokuj bazę danych</string>
<string name="menu_open">Otwórz</string>
<string name="menu_search">Szukaj</string>

View File

@@ -83,8 +83,6 @@
<string name="list_size_summary">Tamanho do texto na lista de grupos</string>
<string name="loading_database">Carregando banco de dados…</string>
<string name="lowercase">Letras minúsculas</string>
<string name="hide_password_title">Esconder senhas</string>
<string name="hide_password_summary">Mascarar senhas (***) por padrão</string>
<string name="about">Sobre</string>
<string name="menu_change_key_settings">Modificar a chave-mestra</string>
<string name="settings">Configurações</string>
@@ -92,7 +90,6 @@
<string name="menu_delete">Deletar</string>
<string name="menu_donate">Doar</string>
<string name="menu_edit">Editar</string>
<string name="menu_hide_password">Esconder senha</string>
<string name="menu_lock">Bloquear banco de dados</string>
<string name="menu_open">Abrir</string>
<string name="menu_search">Buscar</string>

View File

@@ -93,8 +93,6 @@
<string name="list_size_summary">Tamanho do texto na lista de grupos</string>
<string name="loading_database">A carregar a base de dados…</string>
<string name="lowercase">Minúsculas</string>
<string name="hide_password_title">Ocultar palavras-passe</string>
<string name="hide_password_summary">Mascarar palavras-passe (***) por predefinição</string>
<string name="about">Sobre</string>
<string name="menu_change_key_settings">Alterar chave mestra</string>
<string name="settings">Configurações</string>
@@ -102,7 +100,6 @@
<string name="menu_delete">Eliminar</string>
<string name="menu_donate">Donativos</string>
<string name="menu_edit">Editar</string>
<string name="menu_hide_password">Ocultar palavra-chave</string>
<string name="menu_lock">Bloquear base de dados</string>
<string name="menu_open">Abrir</string>
<string name="menu_search">Pesquisar</string>

View File

@@ -142,7 +142,6 @@
<string name="menu_search">Pesquisar</string>
<string name="menu_open">Abrir</string>
<string name="menu_lock">Bloquear base de dados</string>
<string name="menu_hide_password">Ocultar palavra-chave</string>
<string name="menu_edit">Editar</string>
<string name="menu_donate">Donativos</string>
<string name="menu_delete">Eliminar</string>
@@ -246,8 +245,6 @@
<string name="auto_focus_search_summary">Solicitar uma pesquisa quando abrir a base de dados</string>
<string name="auto_focus_search_title">Pesquisa rápida</string>
<string name="about">Sobre</string>
<string name="hide_password_summary">Mascarar palavras-passe (***) por predefinição</string>
<string name="hide_password_title">Ocultar palavras-passe</string>
<string name="html_about_contribution">Para &lt;strong&gt;manter a liberdade&lt;/strong&gt;, &lt;strong&gt;corrigir erros&lt;/strong&gt;, &lt;strong&gt;adicionar funcionalidades&lt;/strong&gt; e &lt;strong&gt;para sermos sempre ativos&lt;/strong&gt;, contamos com sua &lt;strong&gt;contribuição&lt;/strong&gt;.</string>
<string name="homepage">Página inicial</string>
<string name="feedback">Comentários</string>

View File

@@ -163,7 +163,6 @@
<string name="menu_paste">Lipește</string>
<string name="menu_delete">Șterge</string>
<string name="menu_cancel">Anulare</string>
<string name="menu_hide_password">Ascunde parola</string>
<string name="menu_lock">Blocare bază de date</string>
<string name="menu_save_database">Salvare date</string>
<string name="menu_open">Deschidere</string>
@@ -180,8 +179,6 @@
<string name="no_results">Nu există rezultate de căutare</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft este &lt;strong&gt;open source&lt;/strong&gt; și &lt;strong&gt;fără reclame&lt;/strong&gt;. \nEste furnizat ca atare, sub licență &lt;strong&gt;GPLv3&lt;/strong&gt;, fără nicio garanție.</string>
<string name="html_about_contribution">Pentru a ne &lt;strong&gt;păstra libertatea&lt;/strong&gt;, &lt;strong&gt;pentru a remedia erori&lt;/strong&gt;, &lt;strong&gt;pentru a adăuga funcții&lt;/strong&gt; și &lt;strong&gt;pentru a fi mereu activi&lt;/strong&gt;, ne bazăm pe &lt;strong&gt;contribuția&lt;/strong&gt; dvs.</string>
<string name="hide_password_title">Ascundeți parolele</string>
<string name="hide_password_summary">Mascați parolele (***) în mod implicit</string>
<string name="about">Despre</string>
<string name="no_url_handler">Instalați un browser web pentru a deschide această adresă URL.</string>
<string name="select_database_file">Deschideți seiful existent</string>

View File

@@ -93,8 +93,6 @@
<string name="list_size_summary">Размер текста элементов списка</string>
<string name="loading_database">Загрузка базы…</string>
<string name="lowercase">Строчные</string>
<string name="hide_password_title">Скрывать пароли</string>
<string name="hide_password_summary">Скрывать пароли за (***) по умолчанию</string>
<string name="about">О программе</string>
<string name="menu_change_key_settings">Изменить главный пароль</string>
<string name="settings">Настройки</string>
@@ -102,7 +100,6 @@
<string name="menu_delete">Удалить</string>
<string name="menu_donate">Помочь</string>
<string name="menu_edit">Изменить</string>
<string name="menu_hide_password">Скрыть пароль</string>
<string name="menu_lock">Заблокировать базу</string>
<string name="menu_open">Открыть</string>
<string name="menu_search">Поиск</string>

View File

@@ -83,8 +83,6 @@
<string name="list_size_summary">Veľkosť textu v zozname prvkov</string>
<string name="loading_database">Načítava sa databáza…</string>
<string name="lowercase">Malé písmená</string>
<string name="hide_password_title">Skryť heslá</string>
<string name="hide_password_summary">Predvolene maskovať heslá (***)</string>
<string name="about">O Programe</string>
<string name="menu_change_key_settings">Zmeniť hlavný Kľúč</string>
<string name="settings">Nastavenia</string>
@@ -92,7 +90,6 @@
<string name="menu_delete">Zmazať</string>
<string name="menu_donate">Darovať</string>
<string name="menu_edit">Upraviť</string>
<string name="menu_hide_password">Skryť heslo</string>
<string name="menu_lock">Zamknúť databázu</string>
<string name="menu_open">Otvoriť</string>
<string name="menu_search">Hľadať</string>

View File

@@ -193,8 +193,6 @@
<string name="invalid_db_sig">Su kuptua dot formati i bazës së të dhënave.</string>
<string name="keyfile_is_empty">Kartela e kyçit është e zbrazët.</string>
<string name="length">Gjatësi</string>
<string name="hide_password_title">Fshihi fjalëkalimet</string>
<string name="hide_password_summary">Si parazgjedhje, maskoji fjalëkalimet (***)</string>
<string name="colorize_password_title">Ngjyrosi fjalëkalimet</string>
<string name="list_entries_show_username_title">Shfaq emra përdoruesi</string>
<string name="list_groups_show_number_entries_title">Shfaq numër zërash</string>
@@ -211,7 +209,6 @@
<string name="menu_master_key_settings">Rregullime kyçi të përgjithshëm</string>
<string name="menu_copy">Kopjoje</string>
<string name="menu_cancel">Anuloje</string>
<string name="menu_hide_password">Fshihe fjalëkalimin</string>
<string name="menu_lock">Kyçe bazën e të dhënave</string>
<string name="menu_save_database">Ruaji të dhënat</string>
<string name="menu_reload_database">Ringarko të dhënat</string>

View File

@@ -255,8 +255,6 @@
<string name="passphrase">Дугачка лозинка</string>
<string name="invalid_credentials">Није могуће прочитати податке за пријављивање.</string>
<string name="invalid_db_same_uuid">%1$s са истим УУИД %2$s већ постоји.</string>
<string name="hide_password_title">Сакриј лозинке</string>
<string name="hide_password_summary">Подразумевај маскирање лозинки са (***)</string>
<string name="colorize_password_title">Обојите лозинке</string>
<string name="colorize_password_summary">Обојите знакове лозинке по типу</string>
<string name="list_entries_show_username_title">Прикажи корисничка имена</string>
@@ -287,7 +285,6 @@
<string name="master_key_settings_summary">Промена, обнова</string>
<string name="menu_move">Премести</string>
<string name="menu_cancel">Откажи</string>
<string name="menu_hide_password">Сакриј лозинку</string>
<string name="menu_lock">Закључај базу података</string>
<string name="menu_save_database">Сачувај податке</string>
<string name="menu_merge_database">Обједини податке</string>

View File

@@ -92,8 +92,6 @@
<string name="list_size_summary">Textstorlek i grupplistan</string>
<string name="loading_database">Laddar databas…</string>
<string name="lowercase">Gemener</string>
<string name="hide_password_title">Dölj lösenord</string>
<string name="hide_password_summary">Döljer lösenord (***) som standard</string>
<string name="about">Om</string>
<string name="menu_change_key_settings">Byt huvudnyckel</string>
<string name="settings">Inställningar</string>
@@ -101,7 +99,6 @@
<string name="menu_delete">Radera</string>
<string name="menu_donate">Donera</string>
<string name="menu_edit">Redigera</string>
<string name="menu_hide_password">Dölj lösenord</string>
<string name="menu_lock">Lås databas</string>
<string name="menu_open">Öppna</string>
<string name="menu_search">Sök</string>

View File

@@ -278,8 +278,6 @@
<string name="hint_group_name">குழு பெயர்</string>
<string name="invalid_db_same_uuid">அதே uuid %2$s உடன் %1$s ஏற்கனவே உள்ளது.</string>
<string name="invalid_db_sig">தரவுத்தள வடிவமைப்பை அடையாளம் காண முடியவில்லை.</string>
<string name="hide_password_title">கடவுச்சொற்களை மறைக்கவும்</string>
<string name="hide_password_summary">இயல்புநிலையாக கடவுச்சொற்களை (***) மறைக்கவும்</string>
<string name="colorize_password_title">கடவுச்சொற்களை வண்ணமயமாக்குங்கள்</string>
<string name="colorize_password_summary">தட்டச்சு மூலம் கடவுச்சொல் எழுத்துக்களை வண்ணமயமாக்குங்கள்</string>
<string name="list_groups_show_number_entries_summary">ஒரு குழுவில் உள்ளீடுகளின் எண்ணிக்கையைக் காட்டுகிறது</string>
@@ -298,7 +296,6 @@
<string name="menu_security_settings_summary">குறியாக்கம், முக்கிய வழித்தோன்றல் செயல்பாடு</string>
<string name="menu_master_key_settings">முதன்மை விசை அமைப்புகள்</string>
<string name="menu_paste">ஒட்டு</string>
<string name="menu_hide_password">கடவுச்சொல்லை மறைக்கவும்</string>
<string name="menu_merge_database">தேதி செல்கிறது</string>
<string name="menu_open_file_read_and_write">மாற்றியமைக்கக்கூடிய</string>
<string name="menu_restore_entry_history">வரலாற்றை மீட்டமை</string>

View File

@@ -20,7 +20,6 @@
<string name="menu_app_settings">การตั้งค่าแอป</string>
<string name="homepage">หน้าหลัก</string>
<string name="security">ความปลอดภัย</string>
<string name="hide_password_summary">ปิดบังรหัสผ่านเป็น (***) โดยค่าเรื่มต้น</string>
<string name="app_timeout_summary">ระยะเวลาที่ไม่ได้ใช้งานก่อนที่จะทำการล็อกฐานข้อมูล</string>
<string name="settings">การตั้งค่า</string>
<string name="hint_pass">รหัสผ่าน</string>
@@ -32,7 +31,6 @@
<string name="error_pass_gen_type">จำเป็นต้องเลือกชนิดของรหัสผ่านที่จะสร้างอย่างน้อยหนึ่งอย่าง</string>
<string name="generate_password">สร้างรหัสผ่าน</string>
<string name="password">รหัสผ่าน</string>
<string name="hide_password_title">ซ่อนรหัสผ่าน</string>
<string name="hint_conf_pass">ยืนยันรหัสผ่าน</string>
<string name="contact">ติดต่อ</string>
<string name="select_database_file">เปิดคลังรหัสผ่านที่มีอยู่แล้ว</string>
@@ -269,7 +267,6 @@
<string name="list_groups_show_number_entries_summary">แสดงจำนวนรายการในกลุ่ม</string>
<string name="list_size_title">ขนาดของรายการ</string>
<string name="creating_database">กําลังสร้างฐานข้อมูล…</string>
<string name="menu_hide_password">ซ่อนรหัสผ่าน</string>
<string name="menu_keystore_remove_key">ลบกุญแจปลดล็อกของอุปกรณ์</string>
<string name="about">เกี่ยวกับ</string>
<string name="menu_move">ย้าย</string>

View File

@@ -101,8 +101,6 @@
<string name="list_size_summary">Öge listesindeki metin boyutu</string>
<string name="loading_database">Veri tabanı yükleniyor…</string>
<string name="lowercase">Küçük harf</string>
<string name="hide_password_title">Parolaları gizle</string>
<string name="hide_password_summary">Parolaları öntanımlı olarak (***) ile maskele</string>
<string name="about">Hakkında</string>
<string name="menu_change_key_settings">Ana anahtarı değiştir</string>
<string name="copy_field">%1$s kopyalandı</string>
@@ -117,7 +115,6 @@
<string name="menu_paste">Yapıştır</string>
<string name="menu_delete">Sil</string>
<string name="menu_cancel">İptal</string>
<string name="menu_hide_password">Parolayı gizle</string>
<string name="menu_lock">Veri tabanını kilitle</string>
<string name="menu_open"></string>
<string name="menu_search">Ara</string>

View File

@@ -18,7 +18,6 @@
<string name="menu_showpass">Серсүзне күрсәтү</string>
<string name="menu_search">Эзләү</string>
<string name="menu_open">Ачу</string>
<string name="menu_hide_password">Серсүзне яшерү</string>
<string name="menu_move">Күчү</string>
<string name="menu_cancel">Баш тарту</string>
<string name="menu_delete">Бетерү</string>
@@ -27,7 +26,6 @@
<string name="settings">Көйләүләр</string>
<string name="menu_change_key_settings">Мастер ачкычны үзгәртү</string>
<string name="about">Турында</string>
<string name="hide_password_title">Серсүзләрне яшерү</string>
<string name="list_entries_show_username_title">Кулланучы исемнәрен күрсәтү</string>
<string name="password">Серсүз</string>
<string name="hint_pass">Серсүз</string>

View File

@@ -83,8 +83,6 @@
<string name="list_size_summary">Розмір тексту у переліку груп</string>
<string name="loading_database">Завантаження бази даних…</string>
<string name="lowercase">Малі літери</string>
<string name="hide_password_title">Ховати паролі</string>
<string name="hide_password_summary">Типово ховати паролі за (***)</string>
<string name="about">Про KeePassDX</string>
<string name="menu_change_key_settings">Змінити головний ключ</string>
<string name="settings">Налаштування</string>
@@ -92,7 +90,6 @@
<string name="menu_delete">Видалити</string>
<string name="menu_donate">Підтримати</string>
<string name="menu_edit">Змінити</string>
<string name="menu_hide_password">Сховати пароль</string>
<string name="menu_lock">Заблокувати базу даних</string>
<string name="menu_open">Відкрити</string>
<string name="menu_search">Пошук</string>

View File

@@ -231,8 +231,6 @@
<string name="invalid_db_sig">Không thể nhận dạng được định dạng cơ sở dữ liệu.</string>
<string name="keyfile_is_empty">Tệp khóa trống.</string>
<string name="length">Chiều dài</string>
<string name="hide_password_title">Ẩn mật khẩu</string>
<string name="hide_password_summary">Che dấu mật khẩu (***) theo mặc định</string>
<string name="colorize_password_title">Tô màu mật khẩu</string>
<string name="colorize_password_summary">Tô màu các ký tự mật khẩu theo loại</string>
<string name="list_entries_show_username_title">Hiển thị tên người dùng</string>
@@ -271,7 +269,6 @@
<string name="menu_paste">Dán</string>
<string name="menu_delete">Xóa</string>
<string name="menu_cancel">Hủy</string>
<string name="menu_hide_password">Ẩn mật khẩu</string>
<string name="menu_lock">Khóa cơ sở dữ liệu</string>
<string name="menu_save_database">Lưu dữ liệu</string>
<string name="menu_merge_database">Hợp nhất dữ liệu</string>

View File

@@ -82,8 +82,6 @@
<string name="list_size_summary">列表文字大小</string>
<string name="loading_database">正在加载数据库…</string>
<string name="lowercase">小写</string>
<string name="hide_password_title">隐藏密码</string>
<string name="hide_password_summary">默认使用星号(***)隐藏密码</string>
<string name="about">关于</string>
<string name="menu_change_key_settings">更改主密钥</string>
<string name="settings">设置</string>
@@ -91,7 +89,6 @@
<string name="menu_delete">删除</string>
<string name="menu_donate">捐赠</string>
<string name="menu_edit">编辑</string>
<string name="menu_hide_password">隐藏密码</string>
<string name="menu_lock">锁定数据库</string>
<string name="menu_open">打开</string>
<string name="menu_search">搜索</string>

View File

@@ -304,8 +304,6 @@
<string name="hide_broken_locations_title">隱藏已損壞的資料庫路徑</string>
<string name="hide_expired_entries_summary">過期條目將被隱藏</string>
<string name="hide_expired_entries_title">隱藏過期的條目</string>
<string name="hide_password_summary">預設隱藏密碼</string>
<string name="hide_password_title">密碼遮罩</string>
<string name="hint_conf_pass">確認密碼</string>
<string name="hint_generated_password">生成密碼</string>
<string name="hint_group_name">群組名</string>
@@ -424,7 +422,6 @@
<string name="menu_external_icon">外部圖示</string>
<string name="menu_file_selection_read_only">唯讀</string>
<string name="menu_form_filling_settings">表格填入</string>
<string name="menu_hide_password">隱藏密碼</string>
<string name="menu_keystore_remove_key">刪除裝置解鎖密鑰</string>
<string name="menu_lock">鎖定資料庫</string>
<string name="menu_master_key_settings">主密鑰設定</string>

View File

@@ -30,7 +30,6 @@
<declare-styleable name="PasswordView">
<attr name="passwordHint" format="string" />
<attr name="passwordMaxLines" format="integer" />
<attr name="passwordVisible" format="boolean" />
</declare-styleable>
<!-- Specific keyboard attributes -->

View File

@@ -45,6 +45,7 @@
<string name="magic_keyboard_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Magikeyboard</string>
<string name="clipboard_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Clipboard</string>
<string name="passkeys_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Passkeys</string>
<string name="user_verification_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Passkeys#UserVerification</string>
<string name="autofill_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Autofill</string>
<string name="file_manager_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/File-Manager-and-Sync</string>
<string name="html_rose">--,--`--,{@</string>
@@ -68,6 +69,8 @@
<bool name="allow_no_password_default" translatable="false">false</bool>
<string name="delete_entered_password_key" translatable="false">delete_entered_password_key</string>
<bool name="delete_entered_password_default" translatable="false">true</bool>
<string name="user_verification_device_credential_key" translatable="false">user_verification_device_credential_key</string>
<bool name="user_verification_device_credential_default" translatable="false">true</bool>
<string name="enable_auto_save_database_key" translatable="false">enable_auto_save_database_key</string>
<bool name="enable_auto_save_database_default" translatable="false">true</bool>
<string name="enable_keep_screen_on_key" translatable="false">enable_keep_screen_on_key</string>
@@ -76,8 +79,6 @@
<bool name="enable_screenshot_mode_key_default" translatable="false">false</bool>
<string name="auto_focus_search_key" translatable="false">auto_focus_search_key</string>
<bool name="auto_focus_search_default" translatable="false">false</bool>
<string name="subdomain_search_key" translatable="false">subdomain_search_key</string>
<bool name="subdomain_search_default" translatable="false">false</bool>
<string name="app_timeout_key" translatable="false">app_timeout_key</string>
<string name="lock_database_screen_off_key" translatable="false">lock_database_screen_off_key</string>
<bool name="lock_database_screen_off_default" translatable="false">true</bool>
@@ -143,6 +144,10 @@
<bool name="passkeys_backup_eligibility_default" translatable="false">true</bool>
<string name="passkeys_backup_state_key" translatable="false">passkeys_backup_state_key</string>
<bool name="passkeys_backup_state_default" translatable="false">true</bool>
<string name="user_verification_preferred_key" translatable="false">user_verification_preferred_key</string>
<bool name="user_verification_preferred_default" translatable="false">false</bool>
<string name="subdomain_search_key" translatable="false">subdomain_search_key</string>
<bool name="subdomain_search_default" translatable="false">false</bool>
<string name="keyboard_notification_entry_key" translatable="false">keyboard_notification_entry_key</string>
<bool name="keyboard_notification_entry_default" translatable="false">true</bool>
<string name="keyboard_notification_entry_clear_close_key" translatable="false">keyboard_notification_entry_clear_close_key</string>
@@ -199,8 +204,6 @@
<bool name="hide_expired_entries_default" translatable="false">false</bool>
<string name="hide_templates_key" translatable="false">hide_templates_key</string>
<bool name="hide_templates_default" translatable="false">true</bool>
<string name="hide_password_key" translatable="false">hide_password_key</string>
<bool name="hide_password_default" translatable="false">true</bool>
<string name="colorize_password_key" translatable="false">colorize_password_key</string>
<bool name="colorize_password_default" translatable="false">true</bool>
<string name="list_entries_show_username_key" translatable="false">list_entries_show_username_key</string>

View File

@@ -214,7 +214,7 @@
<string name="error_empty_key">Key cannot be empty.</string>
<string name="field_name">Field name</string>
<string name="field_value">Field value</string>
<string name="file_not_found_content">Could not find file. Try reopening it from your file browser.</string>
<string name="file_not_found_content">Could not find file</string>
<string name="corrupted_file">Corrupted file.</string>
<string name="file_browser">File manager</string>
<string name="generate_password">Generate password</string>
@@ -235,8 +235,6 @@
<string name="keyfile_is_empty">The keyfile is empty.</string>
<string name="length">Length</string>
<string name="nodes">Nodes</string>
<string name="hide_password_title">Hide passwords</string>
<string name="hide_password_summary">Mask passwords (***) by default</string>
<string name="colorize_password_title">Colorize passwords</string>
<string name="colorize_password_summary">Colorize password characters by type</string>
<string name="list_entries_show_username_title">Show usernames</string>
@@ -277,7 +275,6 @@
<string name="menu_paste">Paste</string>
<string name="menu_delete">Delete</string>
<string name="menu_cancel">Cancel</string>
<string name="menu_hide_password">Hide password</string>
<string name="menu_lock">Lock database</string>
<string name="menu_save_database">Save data</string>
<string name="menu_merge_database">Merge data</string>
@@ -447,6 +444,10 @@
<string name="autofill_explanation_summary">Configure autofilling to quickly fill out forms in other apps</string>
<string name="autofill_select_entry">Select entry…</string>
<string name="autofill_preference_title">Autofill settings</string>
<string name="user_verification_device_credential_title">Device credential</string>
<string name="user_verification_device_credential_summary">Use the device credential as user verification if available</string>
<string name="user_verification_preferred_title">Preferred User Verification</string>
<string name="user_verification_preferred_summary">Perform a user verification when the relying party requests \"preferred\"</string>
<string name="password_size_title">Generated password size</string>
<string name="password_size_summary">Sets default size of the generated passwords</string>
<string name="list_password_generator_options_title">Password characters</string>
@@ -776,4 +777,10 @@
<string name="passkey_backup_state">Passkey Backup State</string>
<string name="error_passkey_result">Unable to return the passkey</string>
<string name="error_passkey_credential_id">No passkey found with relying party %1$s and credentialIds %2$s</string>
<string name="content_description_user_verification_information">User verification info</string>
<string name="user_verification_required_title">User Verification</string>
<string name="user_verification_required_description">Access to this data requires verification</string>
<string name="user_verification_database_credential">Enter the first four characters of your database password</string>
<string name="first_chars">First chars</string>
<string name="check">Check</string>
</resources>

View File

@@ -288,8 +288,7 @@
<!-- Dialog -->
<style name="KeepassDXStyle.Light.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
<item name="android:windowBackground">?attr/dialogBackgroundColor</item>
<item name="background">?attr/dialogBackgroundColor</item>
<item name="android:colorBackground">?attr/dialogBackgroundColor</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Light.Dialog.NegativeButtonStyle</item>
@@ -303,8 +302,7 @@
</style>
<style name="KeepassDXStyle.Night.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
<item name="android:windowBackground">?attr/dialogBackgroundColor</item>
<item name="background">?attr/dialogBackgroundColor</item>
<item name="android:colorBackground">?attr/dialogBackgroundColor</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Night.Dialog.NegativeButtonStyle</item>

View File

@@ -45,6 +45,11 @@
android:title="@string/show_entry_colors_title"
android:summary="@string/show_entry_colors_summary"
android:defaultValue="@bool/show_entry_colors_default"/>
<SwitchPreferenceCompat
android:key="@string/colorize_password_key"
android:title="@string/colorize_password_title"
android:summary="@string/colorize_password_summary"
android:defaultValue="@bool/colorize_password_default"/>
</PreferenceCategory>
@@ -64,22 +69,6 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/password">
<SwitchPreferenceCompat
android:key="@string/hide_password_key"
android:title="@string/hide_password_title"
android:summary="@string/hide_password_summary"
android:defaultValue="@bool/hide_password_default"/>
<SwitchPreferenceCompat
android:key="@string/colorize_password_key"
android:title="@string/colorize_password_title"
android:summary="@string/colorize_password_summary"
android:defaultValue="@bool/colorize_password_default"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/text_appearance">

View File

@@ -32,6 +32,11 @@
android:title="@string/delete_entered_password_title"
android:summary="@string/delete_entered_password_summary"
android:defaultValue="@bool/delete_entered_password_default"/>
<SwitchPreferenceCompat
android:key="@string/user_verification_device_credential_key"
android:title="@string/user_verification_device_credential_title"
android:summary="@string/user_verification_device_credential_summary"
android:defaultValue="@bool/user_verification_device_credential_default"/>
<SwitchPreferenceCompat
android:key="@string/enable_auto_save_database_key"
android:title="@string/enable_auto_save_database_title"

View File

@@ -20,12 +20,31 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/general">
android:key="@string/credential_provider_key"
android:title="@string/credential_provider">
<SwitchPreferenceCompat
android:key="@string/subdomain_search_key"
android:title="@string/subdomain_search_title"
android:summary="@string/subdomain_search_summary"
android:defaultValue="@bool/subdomain_search_default"/>
android:key="@string/settings_credential_provider_enable_key"
android:title="@string/set_credential_provider_service_title"
android:defaultValue="@bool/settings_credential_provider_enable_default"/>
<SwitchPreferenceCompat
android:key="@string/user_verification_preferred_key"
android:title="@string/user_verification_preferred_title"
android:summary="@string/user_verification_preferred_summary"
android:defaultValue="@bool/user_verification_preferred_default"/>
<Preference
android:key="@string/passkeys_explanation_key"
android:icon="@drawable/prefs_info_24dp"
android:summary="@string/passkeys_explanation_summary"/>
<Preference
android:key="@string/settings_passkeys_key"
android:title="@string/passkeys_preference_title" />
<Preference
android:key="@string/autofill_explanation_key"
android:icon="@drawable/prefs_info_24dp"
android:summary="@string/autofill_explanation_summary"/>
<Preference
android:key="@string/settings_autofill_key"
android:title="@string/autofill_preference_title" />
</PreferenceCategory>
<PreferenceCategory
@@ -43,26 +62,12 @@
</PreferenceCategory>
<PreferenceCategory
android:key="@string/credential_provider_key"
android:title="@string/credential_provider">
android:title="@string/general">
<SwitchPreferenceCompat
android:key="@string/settings_credential_provider_enable_key"
android:title="@string/set_credential_provider_service_title"
android:defaultValue="@bool/settings_credential_provider_enable_default"/>
<Preference
android:key="@string/passkeys_explanation_key"
android:icon="@drawable/prefs_info_24dp"
android:summary="@string/passkeys_explanation_summary"/>
<Preference
android:key="@string/settings_passkeys_key"
android:title="@string/passkeys_preference_title" />
<Preference
android:key="@string/autofill_explanation_key"
android:icon="@drawable/prefs_info_24dp"
android:summary="@string/autofill_explanation_summary"/>
<Preference
android:key="@string/settings_autofill_key"
android:title="@string/autofill_preference_title" />
android:key="@string/subdomain_search_key"
android:title="@string/subdomain_search_title"
android:summary="@string/subdomain_search_summary"
android:defaultValue="@bool/subdomain_search_default"/>
</PreferenceCategory>
<PreferenceCategory

Some files were not shown because too many files have changed in this diff Show More