mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
24 Commits
develop
...
c88413f7f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c88413f7f7 | ||
|
|
7b1fb8a4bf | ||
|
|
3567fa797b | ||
|
|
eb41233e57 | ||
|
|
b394a99e40 | ||
|
|
2bbb40e513 | ||
|
|
09ef69e6ae | ||
|
|
762ac8f77b | ||
|
|
d28087d8d8 | ||
|
|
17d4c363ac | ||
|
|
c754b6a049 | ||
|
|
9c6241afc9 | ||
|
|
f6774b6d51 | ||
|
|
108a61905e | ||
|
|
d251788b1a | ||
|
|
7ed8a44168 | ||
|
|
844b1dfc79 | ||
|
|
d087fcc930 | ||
|
|
5fd25c6150 | ||
|
|
c1cfddddbe | ||
|
|
609b536898 | ||
|
|
f9051ce787 | ||
|
|
d90d175bd8 | ||
|
|
c17fba8ef7 |
@@ -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
|
||||
|
||||
|
||||
@@ -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,13 +543,11 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_edit -> {
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
|
||||
database?.let { database ->
|
||||
entryId?.let { entryId ->
|
||||
EntryEditActivity.launch(
|
||||
activity = this,
|
||||
activity = this@EntryActivity,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
@@ -488,20 +555,41 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_edit -> {
|
||||
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()
|
||||
|
||||
@@ -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,11 +388,10 @@ 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()) {
|
||||
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)
|
||||
@@ -395,11 +399,41 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
|
||||
}
|
||||
.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 ->
|
||||
|
||||
@@ -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,20 +770,18 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
},
|
||||
registrationAction = { intentSenderMode, typeMode, searchInfo ->
|
||||
registrationAction = { _, _, _ ->
|
||||
// Save not used
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coordinatorError?.showActionErrorIfNeeded(result)
|
||||
|
||||
// Reload the group
|
||||
loadGroup()
|
||||
finishNodeAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun manageIntent(intent: Intent?) {
|
||||
intent?.let {
|
||||
@@ -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,
|
||||
if ((node as Entry).getEntryInfo(database).isUserVerificationNeeded()) {
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
dataToVerify = UserVerificationData(
|
||||
actionType = UserVerificationActionType.EDIT_ENTRY,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = (node as Entry).nodeId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
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()
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,23 +58,21 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
databaseUri?.let {
|
||||
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||
it.getDocumentFile(requireContext())?.name
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -100,6 +80,13 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
} ?: run {
|
||||
mMainCredentialViewModel.cancelMainCredential(
|
||||
databaseUri = null,
|
||||
error = FileNotFoundDatabaseException()
|
||||
)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
setOnForegroundColorClickListener {
|
||||
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||
}
|
||||
setOnUnprotectClickListener { _, textEditFieldView ->
|
||||
mEntryEditViewModel.requestUnprotectField(textEditFieldView)
|
||||
}
|
||||
setOnCustomEditionActionClickListener { field ->
|
||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
} else {
|
||||
// Could not be handled here. Try with the super method.
|
||||
else if (otherDialogFragment) {
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
override fun setProtection(
|
||||
protection: Boolean,
|
||||
isCurrentlyProtected: Boolean,
|
||||
onUnprotectClickListener: OnClickListener?
|
||||
) {
|
||||
super.setProtection(protection, isCurrentlyProtected, onUnprotectClickListener)
|
||||
showButton.isVisible = isProtected
|
||||
if (isProtected) {
|
||||
showButton.setOnClickListener {
|
||||
showButton.isSelected = !showButton.isSelected
|
||||
changeProtectedValueParameters()
|
||||
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() {
|
||||
|
||||
@@ -238,15 +238,13 @@ fun View.updateLockPaddingStart() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.toastError(e: Throwable) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
if (e is LocalizedException)
|
||||
fun Context.toastError(e: Throwable?) {
|
||||
val message = if (e is LocalizedException)
|
||||
e.getLocalizedMessage(resources)
|
||||
else
|
||||
e.localizedMessage,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
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 ->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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ę.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 <strong>manter a liberdade</strong>, <strong>corrigir erros</strong>, <strong>adicionar funcionalidades</strong> e <strong>para sermos sempre ativos</strong>, contamos com sua <strong>contribuição</strong>.</string>
|
||||
<string name="homepage">Página inicial</string>
|
||||
<string name="feedback">Comentários</string>
|
||||
|
||||
@@ -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 <strong>open source</strong> și <strong>fără reclame</strong>. \nEste furnizat ca atare, sub licență <strong>GPLv3</strong>, fără nicio garanție.</string>
|
||||
<string name="html_about_contribution">Pentru a ne <strong>păstra libertatea</strong>, <strong>pentru a remedia erori</strong>, <strong>pentru a adăuga funcții</strong> și <strong>pentru a fi mereu activi</strong>, ne bazăm pe <strong>contribuția</strong> 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -193,8 +193,6 @@
|
||||
<string name="invalid_db_sig">S’u 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">Aç</string>
|
||||
<string name="menu_search">Ara</string>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user